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
375struct GlyphCtx {
377 font_size: f64,
378 tc: f64,
379 tw: f64,
380 th: f64,
381}
382
383fn char_bounds_from_bytes(
385 bytes: &[u8],
386 fi: &FontInfo,
387 x: f64,
388 y: f64,
389 ctx: &GlyphCtx,
390) -> (Vec<[f64; 4]>, WidthSource, f64) {
391 let (advances, ws) = compute_glyph_advances(bytes, fi, ctx.font_size, ctx.tc, ctx.tw, ctx.th);
392 let mut cx = x;
393 let mut bounds = Vec::with_capacity(advances.len());
394 for adv in &advances {
395 bounds.push([cx, y, cx + adv, y + ctx.font_size]);
396 cx += adv;
397 }
398 (bounds, ws, cx - x)
399}
400
401fn char_bounds_from_tj_array(
407 arr: &[Object],
408 fi: &FontInfo,
409 x_start: f64,
410 y: f64,
411 ctx: &GlyphCtx,
412) -> (Vec<[f64; 4]>, WidthSource, f64) {
413 let font_size = ctx.font_size;
414 let tc = ctx.tc;
415 let tw = ctx.tw;
416 let th = ctx.th;
417 let has_metrics = fi_has_metrics(fi);
418 let width_source = if has_metrics {
419 WidthSource::Metric
420 } else {
421 WidthSource::Estimate
422 };
423 let approx = APPROX_CHAR_WIDTH * 1000.0_f64;
424
425 let mut bounds = Vec::new();
426 let mut cx = x_start;
427
428 for item in arr {
429 match item {
430 Object::String(bytes, _) => {
431 if fi.is_cid {
432 let mut i = 0;
433 while i + 1 < bytes.len() {
434 let code = u16::from_be_bytes([bytes[i], bytes[i + 1]]) as u32;
435 i += 2;
436 let glyph_w = font_glyph_advance(fi, code).unwrap_or(approx as f32) as f64;
437 let adv = glyph_w / 1000.0 * font_size * (th / 100.0) + tc;
438 bounds.push([cx, y, cx + adv, y + font_size]);
439 cx += adv;
440 }
441 } else {
442 for &b in bytes.iter() {
443 let glyph_w = if has_metrics {
444 font_glyph_advance(fi, b as u32).unwrap_or(approx as f32) as f64
445 } else {
446 approx
447 };
448 let extra_word = if b == 0x20 { tw } else { 0.0 };
449 let adv = glyph_w / 1000.0 * font_size * (th / 100.0) + tc + extra_word;
450 bounds.push([cx, y, cx + adv, y + font_size]);
451 cx += adv;
452 }
453 }
454 }
455 _ => {
456 if let Some(adj) = as_number(item) {
457 cx -= adj / 1000.0 * font_size * (th / 100.0);
459 }
460 }
461 }
462 }
463
464 (bounds, width_source, cx)
465}
466
467fn build_font_map(doc: &Document, page_id: ObjectId) -> HashMap<String, FontInfo> {
469 let mut map = HashMap::new();
470
471 let resources = get_page_resources(doc, page_id);
473 let font_dict = match resources.and_then(|res| match res.get(b"Font").ok()? {
474 Object::Dictionary(d) => Some(d.clone()),
475 Object::Reference(r) => match doc.get_object(*r).ok()? {
476 Object::Dictionary(d) => Some(d.clone()),
477 _ => None,
478 },
479 _ => None,
480 }) {
481 Some(d) => d,
482 None => return map,
483 };
484
485 for (name_bytes, value) in font_dict.iter() {
486 let font_name = String::from_utf8_lossy(name_bytes).to_string();
487
488 let font = match value {
490 Object::Reference(r) => match doc.get_object(*r).ok() {
491 Some(Object::Dictionary(d)) => d.clone(),
492 _ => continue,
493 },
494 Object::Dictionary(d) => d.clone(),
495 _ => continue,
496 };
497
498 let subtype = font
499 .get(b"Subtype")
500 .ok()
501 .and_then(|o| match o {
502 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
503 _ => None,
504 })
505 .unwrap_or_default();
506
507 let is_cid = subtype == "Type0";
508
509 let to_unicode = parse_to_unicode_from_font(doc, &font);
511
512 let to_unicode = if to_unicode.is_empty() && is_cid {
514 if let Ok(Object::Array(descendants)) = font.get(b"DescendantFonts") {
515 descendants
516 .iter()
517 .find_map(|d| {
518 let desc_dict = match d {
519 Object::Reference(r) => match doc.get_object(*r).ok()? {
520 Object::Dictionary(d) => d,
521 _ => return None,
522 },
523 Object::Dictionary(d) => d,
524 _ => return None,
525 };
526 let tu = parse_to_unicode_from_font(doc, desc_dict);
527 if tu.is_empty() {
528 None
529 } else {
530 Some(tu)
531 }
532 })
533 .unwrap_or_default()
534 } else {
535 HashMap::new()
536 }
537 } else {
538 to_unicode
539 };
540
541 let (encoding_map, ct_codes) = if !is_cid {
543 build_encoding_map(doc, &font)
544 } else {
545 ([None; 256], [false; 256])
546 };
547
548 let (base_font, is_bold, is_italic) = derive_font_style(doc, &font);
549
550 let (simple_widths, cid_default_width, cid_widths) = parse_font_widths(doc, &font, is_cid);
551
552 map.insert(
553 font_name,
554 FontInfo {
555 is_cid,
556 to_unicode,
557 encoding_map,
558 ct_codes,
559 base_font,
560 is_bold,
561 is_italic,
562 simple_widths,
563 cid_default_width,
564 cid_widths,
565 },
566 );
567 }
568
569 map
570}
571
572fn parse_font_widths(
579 doc: &Document,
580 font: &lopdf::Dictionary,
581 is_cid: bool,
582) -> (Box<[Option<f32>; 256]>, f32, HashMap<u32, f32>) {
583 if is_cid {
584 let (dw, cid_map) = parse_cid_widths(doc, font);
585 return (Box::new([None; 256]), dw, cid_map);
586 }
587
588 let first_char = match font.get(b"FirstChar").ok() {
590 Some(Object::Integer(n)) => *n as usize,
591 _ => return (Box::new([None; 256]), 1000.0, HashMap::new()),
592 };
593
594 let widths_arr = match font.get(b"Widths").ok() {
595 Some(Object::Array(a)) => a.clone(),
596 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
597 Some(Object::Array(a)) => a.clone(),
598 _ => return (Box::new([None; 256]), 1000.0, HashMap::new()),
599 },
600 _ => return (Box::new([None; 256]), 1000.0, HashMap::new()),
601 };
602
603 let mut simple_widths = Box::new([None::<f32>; 256]);
604 for (i, obj) in widths_arr.iter().enumerate() {
605 let code = first_char + i;
606 if code > 255 {
607 break;
608 }
609 let w = match obj {
610 Object::Integer(n) => *n as f32,
611 Object::Real(n) => *n,
612 _ => continue,
613 };
614 simple_widths[code] = Some(w);
615 }
616
617 (simple_widths, 1000.0, HashMap::new())
618}
619
620fn parse_cid_widths(doc: &Document, font: &lopdf::Dictionary) -> (f32, HashMap<u32, f32>) {
623 let descendants = match font.get(b"DescendantFonts").ok() {
624 Some(Object::Array(a)) => a.clone(),
625 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
626 Some(Object::Array(a)) => a.clone(),
627 _ => return (1000.0, HashMap::new()),
628 },
629 _ => return (1000.0, HashMap::new()),
630 };
631
632 let desc_dict = match descendants.first() {
633 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
634 Some(Object::Dictionary(d)) => d.clone(),
635 _ => return (1000.0, HashMap::new()),
636 },
637 Some(Object::Dictionary(d)) => d.clone(),
638 _ => return (1000.0, HashMap::new()),
639 };
640
641 let dw = match desc_dict.get(b"DW").ok() {
642 Some(Object::Integer(n)) => *n as f32,
643 Some(Object::Real(n)) => *n,
644 _ => 1000.0,
645 };
646
647 let w_arr = match desc_dict.get(b"W").ok() {
648 Some(Object::Array(a)) => a.clone(),
649 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
650 Some(Object::Array(a)) => a.clone(),
651 _ => return (dw, HashMap::new()),
652 },
653 _ => return (dw, HashMap::new()),
654 };
655
656 let mut map = HashMap::new();
658 let mut idx = 0;
659 while idx < w_arr.len() {
660 let cid_start = match &w_arr[idx] {
661 Object::Integer(n) => *n as u32,
662 _ => {
663 idx += 1;
664 continue;
665 }
666 };
667 idx += 1;
668 if idx >= w_arr.len() {
669 break;
670 }
671 match &w_arr[idx] {
672 Object::Array(widths) => {
673 for (i, wobj) in widths.iter().enumerate() {
675 let w = match wobj {
676 Object::Integer(n) => *n as f32,
677 Object::Real(n) => *n,
678 _ => continue,
679 };
680 map.insert(cid_start + i as u32, w);
681 }
682 idx += 1;
683 }
684 Object::Integer(c2) => {
685 let cid_end = *c2 as u32;
687 idx += 1;
688 if idx >= w_arr.len() {
689 break;
690 }
691 let w = match &w_arr[idx] {
692 Object::Integer(n) => *n as f32,
693 Object::Real(n) => *n,
694 _ => {
695 idx += 1;
696 continue;
697 }
698 };
699 for cid in cid_start..=cid_end {
700 map.insert(cid, w);
701 }
702 idx += 1;
703 }
704 _ => {
705 idx += 1;
706 }
707 }
708 }
709
710 (dw, map)
711}
712
713fn parse_to_unicode_from_font(doc: &Document, font: &lopdf::Dictionary) -> HashMap<u32, String> {
715 let tu_obj = match font.get(b"ToUnicode").ok() {
716 Some(Object::Reference(r)) => doc.get_object(*r).ok(),
717 Some(obj) => Some(obj),
718 None => return HashMap::new(),
719 };
720
721 let stream_bytes = match tu_obj {
722 Some(Object::Stream(ref s)) => s
723 .decompressed_content()
724 .ok()
725 .unwrap_or_else(|| s.content.clone()),
726 _ => return HashMap::new(),
727 };
728
729 parse_to_unicode_cmap(&stream_bytes)
730}
731
732fn parse_to_unicode_cmap(data: &[u8]) -> HashMap<u32, String> {
736 let text = String::from_utf8_lossy(data);
737 let mut map = HashMap::new();
738
739 for section in text.split("beginbfchar") {
741 let section = match section.split("endbfchar").next() {
742 Some(s) => s,
743 None => continue,
744 };
745 let tokens = extract_hex_tokens(section);
746 for pair in tokens.chunks(2) {
747 if pair.len() == 2 {
748 let code = parse_hex_u32(&pair[0]);
749 let unicode = hex_to_unicode_string(&pair[1]);
750 map.insert(code, unicode);
751 }
752 }
753 }
754
755 for section in text.split("beginbfrange") {
757 let section = match section.split("endbfrange").next() {
758 Some(s) => s,
759 None => continue,
760 };
761
762 let mut chars = section.chars().peekable();
764 let mut tokens: Vec<String> = Vec::new();
765 let mut arrays: Vec<Vec<String>> = Vec::new();
766 let mut in_array = false;
767 let mut current_array: Vec<String> = Vec::new();
768
769 while let Some(&ch) = chars.peek() {
770 if ch == '<' {
771 chars.next();
772 let hex: String = chars
773 .by_ref()
774 .take_while(|&c| c != '>')
775 .filter(|c| !c.is_whitespace())
776 .collect();
777 if in_array {
778 current_array.push(hex);
779 } else {
780 tokens.push(hex);
781 }
782 } else if ch == '[' {
783 chars.next();
784 in_array = true;
785 current_array = Vec::new();
786 } else if ch == ']' {
787 chars.next();
788 in_array = false;
789 arrays.push(std::mem::take(&mut current_array));
790 tokens.push(String::new()); } else {
792 chars.next();
793 }
794 }
795
796 let mut array_idx = 0;
798 let mut i = 0;
799 while i + 2 < tokens.len() {
800 let lo = parse_hex_u32(&tokens[i]);
801 let hi = parse_hex_u32(&tokens[i + 1]);
802
803 if tokens[i + 2].is_empty() {
804 if array_idx < arrays.len() {
806 let arr = &arrays[array_idx];
807 for (offset, dst) in arr.iter().enumerate() {
808 let code = lo + offset as u32;
809 if code <= hi {
810 map.insert(code, hex_to_unicode_string(dst));
811 }
812 }
813 array_idx += 1;
814 }
815 } else {
816 let dst_start = parse_hex_u32(&tokens[i + 2]);
818 let dst_len = tokens[i + 2].len();
819 for code in lo..=hi {
820 let dst_val = dst_start + (code - lo);
821 let s = if dst_len <= 4 {
822 char::from_u32(dst_val)
824 .map(|c| c.to_string())
825 .unwrap_or_default()
826 } else {
827 let hex = format!("{:0>width$X}", dst_val, width = dst_len);
829 hex_to_unicode_string(&hex)
830 };
831 map.insert(code, s);
832 }
833 }
834 i += 3;
835 }
836 }
837
838 map
839}
840
841fn extract_hex_tokens(text: &str) -> Vec<String> {
843 let mut tokens = Vec::new();
844 let mut in_hex = false;
845 let mut current = String::new();
846 for ch in text.chars() {
847 if ch == '<' {
848 in_hex = true;
849 current.clear();
850 } else if ch == '>' && in_hex {
851 in_hex = false;
852 tokens.push(current.clone());
853 } else if in_hex && !ch.is_whitespace() {
854 current.push(ch);
855 }
856 }
857 tokens
858}
859
860fn parse_hex_u32(hex: &str) -> u32 {
862 u32::from_str_radix(hex, 16).unwrap_or(0)
863}
864
865fn hex_to_unicode_string(hex: &str) -> String {
867 let bytes: Vec<u8> = (0..hex.len())
868 .step_by(2)
869 .filter_map(|i| u8::from_str_radix(&hex[i..i + 2.min(hex.len() - i)], 16).ok())
870 .collect();
871
872 if bytes.len() >= 2 && bytes.len().is_multiple_of(2) {
873 let u16s: Vec<u16> = bytes
875 .chunks(2)
876 .map(|c| u16::from_be_bytes([c[0], c[1]]))
877 .collect();
878 String::from_utf16_lossy(&u16s)
879 } else if bytes.len() == 1 {
880 char::from_u32(bytes[0] as u32)
881 .map(|c| c.to_string())
882 .unwrap_or_default()
883 } else {
884 String::new()
885 }
886}
887
888fn get_page_resources(doc: &Document, page_id: ObjectId) -> Option<lopdf::Dictionary> {
890 let page = match doc.get_object(page_id).ok()? {
891 Object::Dictionary(d) => d.clone(),
892 _ => return None,
893 };
894
895 if let Some(res) = resolve_dict(doc, &page, b"Resources") {
897 return Some(res);
898 }
899
900 let mut current = page;
902 for _ in 0..20 {
903 let parent_ref = match current.get(b"Parent").ok()? {
904 Object::Reference(r) => *r,
905 _ => break,
906 };
907 let parent = match doc.get_object(parent_ref).ok()? {
908 Object::Dictionary(d) => d.clone(),
909 _ => break,
910 };
911 if let Some(res) = resolve_dict(doc, &parent, b"Resources") {
912 return Some(res);
913 }
914 current = parent;
915 }
916
917 None
918}
919
920fn resolve_dict(doc: &Document, dict: &lopdf::Dictionary, key: &[u8]) -> Option<lopdf::Dictionary> {
922 match dict.get(key).ok()? {
923 Object::Dictionary(d) => Some(d.clone()),
924 Object::Reference(r) => match doc.get_object(*r).ok()? {
925 Object::Dictionary(d) => Some(d.clone()),
926 _ => None,
927 },
928 _ => None,
929 }
930}
931
932fn build_encoding_map(
939 doc: &Document,
940 font: &lopdf::Dictionary,
941) -> ([Option<char>; 256], [bool; 256]) {
942 let mut table = [None::<char>; 256];
943 let mut ct_codes = [false; 256];
944
945 let encoding = match font.get(b"Encoding").ok() {
946 Some(obj) => obj,
947 None => return (table, ct_codes),
948 };
949
950 match encoding {
951 Object::Name(name) => {
952 let name_str = String::from_utf8_lossy(name);
954 apply_base_encoding(&mut table, &name_str);
955 }
956 Object::Reference(r) => match doc.get_object(*r) {
957 Ok(Object::Dictionary(enc_dict)) => {
958 parse_encoding_dict(doc, enc_dict, &mut table, &mut ct_codes);
959 }
960 Ok(Object::Name(name)) => {
961 let name_str = String::from_utf8_lossy(name);
962 apply_base_encoding(&mut table, &name_str);
963 }
964 _ => {}
965 },
966 Object::Dictionary(enc_dict) => {
967 parse_encoding_dict(doc, enc_dict, &mut table, &mut ct_codes);
968 }
969 _ => {}
970 }
971
972 (table, ct_codes)
973}
974
975fn parse_encoding_dict(
977 doc: &Document,
978 enc_dict: &lopdf::Dictionary,
979 table: &mut [Option<char>; 256],
980 ct_codes: &mut [bool; 256],
981) {
982 if let Ok(Object::Name(base)) = enc_dict.get(b"BaseEncoding") {
984 let base_str = String::from_utf8_lossy(base);
985 apply_base_encoding(table, &base_str);
986 }
987
988 let diffs = match enc_dict.get(b"Differences").ok() {
990 Some(Object::Array(arr)) => arr.clone(),
991 Some(Object::Reference(r)) => match doc.get_object(*r).ok() {
992 Some(Object::Array(arr)) => arr.clone(),
993 _ => return,
994 },
995 _ => return,
996 };
997
998 let mut code: Option<u32> = None;
999 for item in &diffs {
1000 match item {
1001 Object::Integer(n) => {
1002 code = Some(*n as u32);
1003 }
1004 Object::Name(name) => {
1005 if let Some(c) = code {
1006 if c < 256 {
1007 let glyph = String::from_utf8_lossy(name);
1008 apply_glyph_name(&glyph, c as usize, table, ct_codes);
1009 }
1010 code = Some(c + 1);
1011 }
1012 }
1013 Object::Reference(r) => {
1014 if let Ok(Object::Name(name)) = doc.get_object(*r) {
1016 if let Some(c) = code {
1017 if c < 256 {
1018 let glyph = String::from_utf8_lossy(name);
1019 apply_glyph_name(&glyph, c as usize, table, ct_codes);
1020 }
1021 code = Some(c + 1);
1022 }
1023 }
1024 }
1025 _ => {}
1026 }
1027 }
1028}
1029
1030fn apply_glyph_name(
1036 glyph: &str,
1037 code: usize,
1038 table: &mut [Option<char>; 256],
1039 ct_codes: &mut [bool; 256],
1040) {
1041 if glyph == "ct" {
1042 table[code] = None;
1045 ct_codes[code] = true;
1046 return;
1047 }
1048 if let Some(ch) = glyph_name_to_unicode(glyph) {
1049 table[code] = Some(ch);
1050 ct_codes[code] = false;
1051 }
1052}
1053
1054fn apply_base_encoding(table: &mut [Option<char>; 256], name: &str) {
1056 let source = match name {
1057 "WinAnsiEncoding" => winansi_encoding(),
1058 "MacRomanEncoding" => mac_roman_encoding(),
1059 _ => return,
1060 };
1061 for (i, &ch) in source.iter().enumerate() {
1062 if ch != '\0' {
1063 table[i] = Some(ch);
1064 }
1065 }
1066}
1067
1068fn winansi_encoding() -> &'static [char; 256] {
1070 static TABLE: OnceLock<[char; 256]> = OnceLock::new();
1071 TABLE.get_or_init(|| {
1072 let mut t = ['\0'; 256];
1073 for i in 0x20..=0x7Eu8 {
1075 t[i as usize] = i as char;
1076 }
1077 t[0x09] = '\t';
1079 t[0x0A] = '\n';
1080 t[0x0D] = '\r';
1081 let cp1252: [(u8, char); 27] = [
1083 (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}'), ];
1111 for (code, ch) in cp1252 {
1112 t[code as usize] = ch;
1113 }
1114 for i in 0xA0..=0xFFu16 {
1116 t[i as usize] = char::from_u32(i as u32).unwrap_or('\0');
1117 }
1118 t
1119 })
1120}
1121
1122fn mac_roman_encoding() -> &'static [char; 256] {
1124 static TABLE: OnceLock<[char; 256]> = OnceLock::new();
1125 TABLE.get_or_init(|| {
1126 let mut t = ['\0'; 256];
1127 for i in 0x20..=0x7Eu8 {
1129 t[i as usize] = i as char;
1130 }
1131 t[0x09] = '\t';
1132 t[0x0A] = '\n';
1133 t[0x0D] = '\r';
1134 let mac_upper: [char; 128] = [
1136 '\u{00C4}', '\u{00C5}', '\u{00C7}', '\u{00C9}', '\u{00D1}', '\u{00D6}', '\u{00DC}',
1137 '\u{00E1}', '\u{00E0}', '\u{00E2}', '\u{00E4}', '\u{00E3}', '\u{00E5}', '\u{00E7}',
1138 '\u{00E9}', '\u{00E8}', '\u{00EA}', '\u{00EB}', '\u{00ED}', '\u{00EC}', '\u{00EE}',
1139 '\u{00EF}', '\u{00F1}', '\u{00F3}', '\u{00F2}', '\u{00F4}', '\u{00F6}', '\u{00F5}',
1140 '\u{00FA}', '\u{00F9}', '\u{00FB}', '\u{00FC}', '\u{2020}', '\u{00B0}', '\u{00A2}',
1141 '\u{00A3}', '\u{00A7}', '\u{2022}', '\u{00B6}', '\u{00DF}', '\u{00AE}', '\u{00A9}',
1142 '\u{2122}', '\u{00B4}', '\u{00A8}', '\u{2260}', '\u{00C6}', '\u{00D8}', '\u{221E}',
1143 '\u{00B1}', '\u{2264}', '\u{2265}', '\u{00A5}', '\u{00B5}', '\u{2202}', '\u{2211}',
1144 '\u{220F}', '\u{03C0}', '\u{222B}', '\u{00AA}', '\u{00BA}', '\u{03A9}', '\u{00E6}',
1145 '\u{00F8}', '\u{00BF}', '\u{00A1}', '\u{00AC}', '\u{221A}', '\u{0192}', '\u{2248}',
1146 '\u{2206}', '\u{00AB}', '\u{00BB}', '\u{2026}', '\u{00A0}', '\u{00C0}', '\u{00C3}',
1147 '\u{00D5}', '\u{0152}', '\u{0153}', '\u{2013}', '\u{2014}', '\u{201C}', '\u{201D}',
1148 '\u{2018}', '\u{2019}', '\u{00F7}', '\u{25CA}', '\u{00FF}', '\u{0178}', '\u{2044}',
1149 '\u{20AC}', '\u{2039}', '\u{203A}', '\u{FB01}', '\u{FB02}', '\u{2021}', '\u{00B7}',
1150 '\u{201A}', '\u{201E}', '\u{2030}', '\u{00C2}', '\u{00CA}', '\u{00C1}', '\u{00CB}',
1151 '\u{00C8}', '\u{00CD}', '\u{00CE}', '\u{00CF}', '\u{00CC}', '\u{00D3}', '\u{00D4}',
1152 '\u{F8FF}', '\u{00D2}', '\u{00DA}', '\u{00DB}', '\u{00D9}', '\u{0131}', '\u{02C6}',
1153 '\u{02DC}', '\u{00AF}', '\u{02D8}', '\u{02D9}', '\u{02DA}', '\u{00B8}', '\u{02DD}',
1154 '\u{02DB}', '\u{02C7}',
1155 ];
1156 for (i, &ch) in mac_upper.iter().enumerate() {
1157 t[0x80 + i] = ch;
1158 }
1159 t
1160 })
1161}
1162
1163fn glyph_name_to_unicode(name: &str) -> Option<char> {
1174 if name.starts_with("uni") && name.len() >= 7 {
1176 return u32::from_str_radix(&name[3..7], 16)
1177 .ok()
1178 .and_then(char::from_u32);
1179 }
1180 if name.starts_with('u') && name.len() >= 5 && name[1..].chars().all(|c| c.is_ascii_hexdigit())
1181 {
1182 return u32::from_str_radix(&name[1..], 16)
1183 .ok()
1184 .and_then(char::from_u32);
1185 }
1186
1187 if let Some(c) = agl_table().get(name).copied() {
1188 return Some(c);
1189 }
1190
1191 match name {
1192 "st" => Some('\u{FB06}'),
1193 "longst" => Some('\u{FB05}'),
1194 _ => None,
1195 }
1196}
1197
1198fn agl_table() -> &'static HashMap<&'static str, char> {
1200 static TABLE: OnceLock<HashMap<&'static str, char>> = OnceLock::new();
1201 TABLE.get_or_init(|| {
1202 let entries: &[(&str, char)] = &[
1203 ("space", ' '),
1204 ("exclam", '!'),
1205 ("quotedbl", '"'),
1206 ("numbersign", '#'),
1207 ("dollar", '$'),
1208 ("percent", '%'),
1209 ("ampersand", '&'),
1210 ("quotesingle", '\''),
1211 ("parenleft", '('),
1212 ("parenright", ')'),
1213 ("asterisk", '*'),
1214 ("plus", '+'),
1215 ("comma", ','),
1216 ("hyphen", '-'),
1217 ("period", '.'),
1218 ("slash", '/'),
1219 ("zero", '0'),
1220 ("one", '1'),
1221 ("two", '2'),
1222 ("three", '3'),
1223 ("four", '4'),
1224 ("five", '5'),
1225 ("six", '6'),
1226 ("seven", '7'),
1227 ("eight", '8'),
1228 ("nine", '9'),
1229 ("colon", ':'),
1230 ("semicolon", ';'),
1231 ("less", '<'),
1232 ("equal", '='),
1233 ("greater", '>'),
1234 ("question", '?'),
1235 ("at", '@'),
1236 ("A", 'A'),
1237 ("B", 'B'),
1238 ("C", 'C'),
1239 ("D", 'D'),
1240 ("E", 'E'),
1241 ("F", 'F'),
1242 ("G", 'G'),
1243 ("H", 'H'),
1244 ("I", 'I'),
1245 ("J", 'J'),
1246 ("K", 'K'),
1247 ("L", 'L'),
1248 ("M", 'M'),
1249 ("N", 'N'),
1250 ("O", 'O'),
1251 ("P", 'P'),
1252 ("Q", 'Q'),
1253 ("R", 'R'),
1254 ("S", 'S'),
1255 ("T", 'T'),
1256 ("U", 'U'),
1257 ("V", 'V'),
1258 ("W", 'W'),
1259 ("X", 'X'),
1260 ("Y", 'Y'),
1261 ("Z", 'Z'),
1262 ("bracketleft", '['),
1263 ("backslash", '\\'),
1264 ("bracketright", ']'),
1265 ("asciicircum", '^'),
1266 ("underscore", '_'),
1267 ("grave", '`'),
1268 ("a", 'a'),
1269 ("b", 'b'),
1270 ("c", 'c'),
1271 ("d", 'd'),
1272 ("e", 'e'),
1273 ("f", 'f'),
1274 ("g", 'g'),
1275 ("h", 'h'),
1276 ("i", 'i'),
1277 ("j", 'j'),
1278 ("k", 'k'),
1279 ("l", 'l'),
1280 ("m", 'm'),
1281 ("n", 'n'),
1282 ("o", 'o'),
1283 ("p", 'p'),
1284 ("q", 'q'),
1285 ("r", 'r'),
1286 ("s", 's'),
1287 ("t", 't'),
1288 ("u", 'u'),
1289 ("v", 'v'),
1290 ("w", 'w'),
1291 ("x", 'x'),
1292 ("y", 'y'),
1293 ("z", 'z'),
1294 ("braceleft", '{'),
1295 ("bar", '|'),
1296 ("braceright", '}'),
1297 ("asciitilde", '~'),
1298 ("Agrave", '\u{00C0}'),
1300 ("Aacute", '\u{00C1}'),
1301 ("Acircumflex", '\u{00C2}'),
1302 ("Atilde", '\u{00C3}'),
1303 ("Adieresis", '\u{00C4}'),
1304 ("Aring", '\u{00C5}'),
1305 ("AE", '\u{00C6}'),
1306 ("Ccedilla", '\u{00C7}'),
1307 ("Egrave", '\u{00C8}'),
1308 ("Eacute", '\u{00C9}'),
1309 ("Ecircumflex", '\u{00CA}'),
1310 ("Edieresis", '\u{00CB}'),
1311 ("Igrave", '\u{00CC}'),
1312 ("Iacute", '\u{00CD}'),
1313 ("Icircumflex", '\u{00CE}'),
1314 ("Idieresis", '\u{00CF}'),
1315 ("Eth", '\u{00D0}'),
1316 ("Ntilde", '\u{00D1}'),
1317 ("Ograve", '\u{00D2}'),
1318 ("Oacute", '\u{00D3}'),
1319 ("Ocircumflex", '\u{00D4}'),
1320 ("Otilde", '\u{00D5}'),
1321 ("Odieresis", '\u{00D6}'),
1322 ("Ugrave", '\u{00D9}'),
1323 ("Uacute", '\u{00DA}'),
1324 ("Ucircumflex", '\u{00DB}'),
1325 ("Udieresis", '\u{00DC}'),
1326 ("Yacute", '\u{00DD}'),
1327 ("Thorn", '\u{00DE}'),
1328 ("germandbls", '\u{00DF}'),
1329 ("agrave", '\u{00E0}'),
1330 ("aacute", '\u{00E1}'),
1331 ("acircumflex", '\u{00E2}'),
1332 ("atilde", '\u{00E3}'),
1333 ("adieresis", '\u{00E4}'),
1334 ("aring", '\u{00E5}'),
1335 ("ae", '\u{00E6}'),
1336 ("ccedilla", '\u{00E7}'),
1337 ("egrave", '\u{00E8}'),
1338 ("eacute", '\u{00E9}'),
1339 ("ecircumflex", '\u{00EA}'),
1340 ("edieresis", '\u{00EB}'),
1341 ("igrave", '\u{00EC}'),
1342 ("iacute", '\u{00ED}'),
1343 ("icircumflex", '\u{00EE}'),
1344 ("idieresis", '\u{00EF}'),
1345 ("eth", '\u{00F0}'),
1346 ("ntilde", '\u{00F1}'),
1347 ("ograve", '\u{00F2}'),
1348 ("oacute", '\u{00F3}'),
1349 ("ocircumflex", '\u{00F4}'),
1350 ("otilde", '\u{00F5}'),
1351 ("odieresis", '\u{00F6}'),
1352 ("ugrave", '\u{00F9}'),
1353 ("uacute", '\u{00FA}'),
1354 ("ucircumflex", '\u{00FB}'),
1355 ("udieresis", '\u{00FC}'),
1356 ("yacute", '\u{00FD}'),
1357 ("thorn", '\u{00FE}'),
1358 ("ydieresis", '\u{00FF}'),
1359 ("fi", '\u{FB01}'),
1361 ("fl", '\u{FB02}'),
1362 ("ff", '\u{FB00}'),
1363 ("ffi", '\u{FB03}'),
1364 ("ffl", '\u{FB04}'),
1365 ("endash", '\u{2013}'),
1367 ("emdash", '\u{2014}'),
1368 ("bullet", '\u{2022}'),
1369 ("ellipsis", '\u{2026}'),
1370 ("quoteleft", '\u{2018}'),
1371 ("quoteright", '\u{2019}'),
1372 ("quotedblleft", '\u{201C}'),
1373 ("quotedblright", '\u{201D}'),
1374 ("quotesinglebase", '\u{201A}'),
1375 ("quotesinglbase", '\u{201A}'),
1376 ("quotedblbase", '\u{201E}'),
1377 ("dagger", '\u{2020}'),
1378 ("daggerdbl", '\u{2021}'),
1379 ("perthousand", '\u{2030}'),
1380 ("guilsinglleft", '\u{2039}'),
1381 ("guilsinglright", '\u{203A}'),
1382 ("guillemotleft", '\u{00AB}'),
1383 ("guillemotright", '\u{00BB}'),
1384 ("trademark", '\u{2122}'),
1385 ("copyright", '\u{00A9}'),
1386 ("registered", '\u{00AE}'),
1387 ("degree", '\u{00B0}'),
1388 ("plusminus", '\u{00B1}'),
1389 ("multiply", '\u{00D7}'),
1390 ("divide", '\u{00F7}'),
1391 ("fraction", '\u{2044}'),
1392 ("Euro", '\u{20AC}'),
1393 ("sterling", '\u{00A3}'),
1394 ("yen", '\u{00A5}'),
1395 ("cent", '\u{00A2}'),
1396 ("currency", '\u{00A4}'),
1397 ("section", '\u{00A7}'),
1398 ("paragraph", '\u{00B6}'),
1399 ("brokenbar", '\u{00A6}'),
1400 ("ordfeminine", '\u{00AA}'),
1401 ("ordmasculine", '\u{00BA}'),
1402 ("exclamdown", '\u{00A1}'),
1403 ("questiondown", '\u{00BF}'),
1404 ("logicalnot", '\u{00AC}'),
1405 ("mu", '\u{00B5}'),
1406 ("macron", '\u{00AF}'),
1407 ("acute", '\u{00B4}'),
1408 ("cedilla", '\u{00B8}'),
1409 ("dieresis", '\u{00A8}'),
1410 ("circumflex", '\u{02C6}'),
1411 ("tilde", '\u{02DC}'),
1412 ("caron", '\u{02C7}'),
1413 ("ring", '\u{02DA}'),
1414 ("breve", '\u{02D8}'),
1415 ("dotaccent", '\u{02D9}'),
1416 ("hungarumlaut", '\u{02DD}'),
1417 ("ogonek", '\u{02DB}'),
1418 ("nbspace", '\u{00A0}'),
1419 ("nonbreakingspace", '\u{00A0}'),
1420 ("softhyphen", '\u{00AD}'),
1421 ("periodcentered", '\u{00B7}'),
1422 ("middot", '\u{00B7}'),
1423 ("florin", '\u{0192}'),
1424 ("OE", '\u{0152}'),
1425 ("oe", '\u{0153}'),
1426 ("Scaron", '\u{0160}'),
1427 ("scaron", '\u{0161}'),
1428 ("Zcaron", '\u{017D}'),
1429 ("zcaron", '\u{017E}'),
1430 ("Ydieresis", '\u{0178}'),
1431 ("Lslash", '\u{0141}'),
1432 ("lslash", '\u{0142}'),
1433 ("Oslash", '\u{00D8}'),
1434 ("oslash", '\u{00F8}'),
1435 ("dotlessi", '\u{0131}'),
1436 ("onesuperior", '\u{00B9}'),
1438 ("twosuperior", '\u{00B2}'),
1439 ("threesuperior", '\u{00B3}'),
1440 ("onequarter", '\u{00BC}'),
1441 ("onehalf", '\u{00BD}'),
1442 ("threequarters", '\u{00BE}'),
1443 ("minus", '\u{2212}'),
1445 ("notequal", '\u{2260}'),
1446 ("lessequal", '\u{2264}'),
1447 ("greaterequal", '\u{2265}'),
1448 ("infinity", '\u{221E}'),
1449 ("partialdiff", '\u{2202}'),
1450 ("summation", '\u{2211}'),
1451 ("product", '\u{220F}'),
1452 ("integral", '\u{222B}'),
1453 ("radical", '\u{221A}'),
1454 ("approxequal", '\u{2248}'),
1455 ("Delta", '\u{0394}'),
1456 ("lozenge", '\u{25CA}'),
1457 ("pi", '\u{03C0}'),
1458 ("Omega", '\u{03A9}'),
1459 ];
1460 entries.iter().cloned().collect()
1461 })
1462}
1463
1464const CT_LIGATURE_MARKER: char = '\u{E007}';
1465
1466#[derive(Clone, Default)]
1467struct DecodedPdfString {
1468 text: String,
1469 ct_origins: Vec<bool>,
1470}
1471
1472impl DecodedPdfString {
1473 fn from_text(text: String) -> Self {
1474 let ct_origins = vec![false; text.chars().count()];
1475 Self { text, ct_origins }
1476 }
1477
1478 fn push_char(&mut self, ch: char, is_ct_origin: bool) {
1479 self.text.push(ch);
1480 self.ct_origins.push(is_ct_origin);
1481 }
1482
1483 fn push_str(&mut self, s: &str) {
1484 for ch in s.chars() {
1485 self.push_char(ch, false);
1486 }
1487 }
1488
1489 fn extend(&mut self, other: DecodedPdfString) {
1490 self.text.push_str(&other.text);
1491 self.ct_origins.extend(other.ct_origins);
1492 }
1493
1494 fn is_empty(&self) -> bool {
1495 self.text.is_empty()
1496 }
1497
1498 fn glyph_count(&self) -> usize {
1499 self.ct_origins.len()
1500 }
1501
1502 fn iter(&self) -> impl Iterator<Item = (char, bool)> + '_ {
1503 self.text.chars().zip(self.ct_origins.iter().copied())
1504 }
1505}
1506
1507fn decode_pdf_string_with_font_marked(
1510 bytes: &[u8],
1511 font_info: Option<&FontInfo>,
1512) -> DecodedPdfString {
1513 if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
1515 let chars: Vec<u16> = bytes[2..]
1516 .chunks(2)
1517 .filter_map(|chunk| {
1518 if chunk.len() == 2 {
1519 Some(u16::from_be_bytes([chunk[0], chunk[1]]))
1520 } else {
1521 None
1522 }
1523 })
1524 .collect();
1525 return DecodedPdfString::from_text(String::from_utf16_lossy(&chars));
1526 }
1527
1528 if let Some(info) = font_info {
1529 if info.is_cid && !info.to_unicode.is_empty() {
1530 let mut result = DecodedPdfString::default();
1532 let mut i = 0;
1533 while i + 1 < bytes.len() {
1534 let code = u16::from_be_bytes([bytes[i], bytes[i + 1]]) as u32;
1535 if let Some(s) = info.to_unicode.get(&code) {
1536 result.push_str(s);
1537 } else {
1538 if let Some(ch) = char::from_u32(code) {
1540 if !ch.is_control() || ch == ' ' || ch == '\t' || ch == '\n' {
1541 result.push_char(ch, false);
1542 }
1543 }
1544 }
1545 i += 2;
1546 }
1547 return result;
1548 }
1549
1550 if !info.is_cid && !info.to_unicode.is_empty() {
1551 let mut result = DecodedPdfString::default();
1553 for &b in bytes {
1554 if let Some(s) = info.to_unicode.get(&(b as u32)) {
1555 result.push_str(s);
1556 } else if info.ct_codes[b as usize] {
1557 result.push_char(CT_LIGATURE_MARKER, true);
1558 } else if let Some(ch) = info.encoding_map[b as usize] {
1559 result.push_char(ch, false);
1560 } else {
1561 let ch = b as char;
1562 if is_printable_or_space(ch) {
1563 result.push_char(ch, false);
1564 }
1565 }
1566 }
1567 return result;
1568 }
1569
1570 if !info.is_cid
1572 && (info.encoding_map.iter().any(|c| c.is_some()) || info.ct_codes.iter().any(|f| *f))
1573 {
1574 let mut result = DecodedPdfString::default();
1575 for &b in bytes {
1576 if info.ct_codes[b as usize] {
1577 result.push_char(CT_LIGATURE_MARKER, true);
1578 } else if let Some(ch) = info.encoding_map[b as usize] {
1579 result.push_char(ch, false);
1580 } else {
1581 let ch = b as char;
1582 if is_printable_or_space(ch) {
1583 result.push_char(ch, false);
1584 }
1585 }
1586 }
1587 return result;
1588 }
1589 }
1590
1591 let mut result = DecodedPdfString::default();
1593 for &b in bytes {
1594 let ch = b as char;
1595 if is_printable_or_space(ch) {
1596 result.push_char(ch, false);
1597 }
1598 }
1599 result
1600}
1601
1602fn is_printable_or_space(ch: char) -> bool {
1605 let cp = ch as u32;
1606 cp >= 0x20 || cp == 0x09 || cp == 0x0A || cp == 0x0D
1607}
1608
1609const LIGATURE_DECOMP: bool = true;
1617
1618fn decompose_ligature_char(c: char) -> Option<&'static str> {
1626 Some(match c {
1627 '\u{FB00}' => "ff",
1628 '\u{FB01}' => "fi",
1629 '\u{FB02}' => "fl",
1630 '\u{FB03}' => "ffi",
1631 '\u{FB04}' => "ffl",
1632 '\u{FB05}' | '\u{FB06}' => "st",
1636 _ => return None,
1637 })
1638}
1639
1640fn decompose_ligature_char_with_origin(c: char, is_ct_origin: bool) -> Option<&'static str> {
1641 if is_ct_origin && c == CT_LIGATURE_MARKER {
1642 Some("ct")
1643 } else {
1644 decompose_ligature_char(c)
1645 }
1646}
1647
1648fn decompose_glyph_to_string(c: char) -> Option<String> {
1656 use unicode_normalization::UnicodeNormalization;
1657 if let Some(s) = decompose_ligature_char(c) {
1658 return Some(s.to_string());
1659 }
1660 if matches!(c, '\u{FB00}'..='\u{FB4F}') {
1661 let nfkd: String = c.nfkd().collect();
1662 if nfkd != c.to_string() {
1667 return Some(nfkd);
1668 }
1669 }
1670 None
1671}
1672
1673fn decompose_ligatures(s: &str) -> String {
1680 let mut out = String::with_capacity(s.len());
1681 for c in s.chars() {
1682 if let Some(replacement) = decompose_glyph_to_string(c) {
1683 out.push_str(&replacement);
1684 } else {
1685 out.push(c);
1686 }
1687 }
1688 out
1689}
1690
1691fn decompose_decoded_ligatures(s: &DecodedPdfString) -> String {
1692 let mut out = String::with_capacity(s.text.len());
1693 for (c, is_ct_origin) in s.iter() {
1694 if let Some(replacement) = decompose_ligature_char_with_origin(c, is_ct_origin) {
1695 out.push_str(replacement);
1696 } else if let Some(replacement) = decompose_glyph_to_string(c) {
1697 out.push_str(&replacement);
1698 } else {
1699 out.push(c);
1700 }
1701 }
1702 out
1703}
1704
1705fn maybe_decompose_decoded(s: &DecodedPdfString) -> String {
1709 if LIGATURE_DECOMP {
1710 if s.ct_origins.iter().any(|origin| *origin) {
1711 decompose_decoded_ligatures(s)
1712 } else {
1713 decompose_ligatures(&s.text)
1714 }
1715 } else {
1716 s.text.clone()
1717 }
1718}
1719
1720pub fn extract_page_blocks(doc: &Document, page_num: u32) -> Vec<TextBlock> {
1722 let pages = doc.get_pages();
1723 let Some(&page_id) = pages.get(&page_num) else {
1724 return Vec::new();
1725 };
1726
1727 let font_map = build_font_map(doc, page_id);
1728 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1729 if let Ok(content_bytes) = get_page_content_bytes(doc, page_id) {
1730 if let Ok(content) = Content::decode(&content_bytes) {
1731 return extract_blocks_from_ops_inner(
1732 &content.operations,
1733 page_num,
1734 &font_map,
1735 Some((doc, &resources)),
1736 0,
1737 None,
1738 );
1739 }
1740 }
1741
1742 Vec::new()
1743}
1744
1745pub fn extract_blocks_from_page_id(
1751 doc: &Document,
1752 page_id: lopdf::ObjectId,
1753 page_num: u32,
1754) -> Vec<TextBlock> {
1755 let font_map = build_font_map(doc, page_id);
1756 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1757 if let Ok(content_bytes) = get_page_content_bytes(doc, page_id) {
1758 if let Ok(content) = Content::decode(&content_bytes) {
1759 return extract_blocks_from_ops_inner(
1760 &content.operations,
1761 page_num,
1762 &font_map,
1763 Some((doc, &resources)),
1764 0,
1765 None,
1766 );
1767 }
1768 }
1769 Vec::new()
1770}
1771
1772pub fn extract_text(doc: &Document) -> Vec<TextBlock> {
1774 let pages = doc.get_pages();
1775 let mut blocks = Vec::new();
1776
1777 for (&page_num, &page_id) in &pages {
1778 let font_map = build_font_map(doc, page_id);
1779 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1780 if let Ok(content_bytes) = get_page_content_bytes(doc, page_id) {
1781 if let Ok(content) = Content::decode(&content_bytes) {
1782 let page_blocks = extract_blocks_from_ops_inner(
1783 &content.operations,
1784 page_num,
1785 &font_map,
1786 Some((doc, &resources)),
1787 0,
1788 None,
1789 );
1790 blocks.extend(page_blocks);
1791 }
1792 }
1793 }
1794
1795 blocks
1796}
1797
1798pub fn extract_page_text(doc: &Document, page_num: u32) -> Result<String> {
1800 let pages = doc.get_pages();
1801 let total = pages.len() as u32;
1802
1803 if page_num == 0 || page_num > total {
1804 return Err(ExtractError::PageOutOfRange(page_num, total));
1805 }
1806
1807 let page_id = *pages
1808 .get(&page_num)
1809 .ok_or(ExtractError::PageOutOfRange(page_num, total))?;
1810
1811 let font_map = build_font_map(doc, page_id);
1812 let resources = get_page_resources(doc, page_id).unwrap_or_default();
1813 let content_bytes = get_page_content_bytes(doc, page_id).unwrap_or_default();
1814 let content = match Content::decode(&content_bytes) {
1815 Ok(c) => c,
1816 Err(_) => return Ok(String::new()),
1817 };
1818
1819 let blocks = extract_blocks_from_ops_inner(
1820 &content.operations,
1821 page_num,
1822 &font_map,
1823 Some((doc, &resources)),
1824 0,
1825 None,
1826 );
1827 let text = blocks
1828 .iter()
1829 .map(|b| b.text.as_str())
1830 .collect::<Vec<_>>()
1831 .join("");
1832
1833 Ok(text)
1834}
1835
1836pub fn extract_positioned_chars(doc: &Document, page_num: u32) -> Result<Vec<PositionedChar>> {
1838 let pages = doc.get_pages();
1839 let total = pages.len() as u32;
1840
1841 if page_num == 0 || page_num > total {
1842 return Err(ExtractError::PageOutOfRange(page_num, total));
1843 }
1844
1845 let page_id = *pages
1846 .get(&page_num)
1847 .ok_or(ExtractError::PageOutOfRange(page_num, total))?;
1848
1849 let font_map = build_font_map(doc, page_id);
1850 let content_bytes = get_page_content_bytes(doc, page_id).unwrap_or_default();
1851 let content = match Content::decode(&content_bytes) {
1852 Ok(c) => c,
1853 Err(_) => return Ok(Vec::new()),
1854 };
1855
1856 let chars = extract_chars_from_ops(&content.operations, page_num, &font_map);
1857 Ok(chars)
1858}
1859
1860fn get_page_content_bytes(doc: &Document, page_id: ObjectId) -> std::result::Result<Vec<u8>, ()> {
1862 Ok(doc.get_page_content(page_id))
1863}
1864
1865#[derive(Debug, Clone, Default)]
1870struct MarkedContentEntry {
1871 actual_text: Option<String>,
1874}
1875
1876fn resolve_bdc_properties(
1880 doc_and_resources: Option<(&Document, &lopdf::Dictionary)>,
1881 operand: &Object,
1882) -> Option<lopdf::Dictionary> {
1883 match operand {
1884 Object::Dictionary(d) => Some(d.clone()),
1885 Object::Reference(r) => doc_and_resources.and_then(|(doc, _)| {
1886 doc.get_object(*r).ok().and_then(|o| match o {
1887 Object::Dictionary(d) => Some(d.clone()),
1888 _ => None,
1889 })
1890 }),
1891 Object::Name(n) => {
1892 let (doc, resources) = doc_and_resources?;
1893 let props = resolve_dict(doc, resources, b"Properties")?;
1894 match props.get(n.as_slice()).ok()? {
1895 Object::Dictionary(d) => Some(d.clone()),
1896 Object::Reference(r) => match doc.get_object(*r).ok()? {
1897 Object::Dictionary(d) => Some(d.clone()),
1898 _ => None,
1899 },
1900 _ => None,
1901 }
1902 }
1903 _ => None,
1904 }
1905}
1906
1907fn decode_pdf_text_string(bytes: &[u8]) -> String {
1911 if bytes.len() >= 2 && bytes[0] == 0xFE && bytes[1] == 0xFF {
1912 let u16s: Vec<u16> = bytes[2..]
1913 .chunks_exact(2)
1914 .map(|c| u16::from_be_bytes([c[0], c[1]]))
1915 .collect();
1916 return String::from_utf16_lossy(&u16s);
1917 }
1918 if bytes.len() >= 3 && bytes[0] == 0xEF && bytes[1] == 0xBB && bytes[2] == 0xBF {
1919 return String::from_utf8_lossy(&bytes[3..]).into_owned();
1920 }
1921 bytes
1922 .iter()
1923 .filter_map(|&b| {
1924 let ch = b as char;
1925 if is_printable_or_space(ch) {
1926 Some(ch)
1927 } else {
1928 None
1929 }
1930 })
1931 .collect()
1932}
1933
1934fn extract_actual_text(props: &lopdf::Dictionary) -> Option<String> {
1936 let obj = props.get(b"ActualText").ok()?;
1937 match obj {
1938 Object::String(bytes, _) => Some(decode_pdf_text_string(bytes)),
1939 _ => None,
1940 }
1941}
1942
1943fn current_actual_text(stack: &[MarkedContentEntry], inherited: Option<&str>) -> Option<String> {
1949 stack
1950 .iter()
1951 .rev()
1952 .find_map(|e| e.actual_text.clone())
1953 .or_else(|| inherited.map(|s| s.to_string()))
1954}
1955
1956fn extract_blocks_from_ops_inner(
1962 ops: &[Operation],
1963 page: u32,
1964 font_map: &HashMap<String, FontInfo>,
1965 doc_and_resources: Option<(&Document, &lopdf::Dictionary)>,
1966 depth: u32,
1967 inherited_actual_text: Option<&str>,
1968) -> Vec<TextBlock> {
1969 let mut state = TextState::default();
1970 let mut blocks = Vec::new();
1971 let mut mc_stack: Vec<MarkedContentEntry> = Vec::new();
1972
1973 for op in ops {
1974 match op.operator.as_str() {
1975 "q" => {
1976 state.gs_stack.push(GraphicsState {
1977 ctm: state.ctm,
1978 fill_color: state.fill_color,
1979 });
1980 }
1981 "Q" => {
1982 if let Some(gs) = state.gs_stack.pop() {
1983 state.ctm = gs.ctm;
1984 state.fill_color = gs.fill_color;
1985 }
1986 }
1987 "cm" => {
1988 if let Some(m) = extract_matrix(&op.operands) {
1989 state.ctm = multiply_matrix(&state.ctm, &m);
1990 }
1991 }
1992 "rg" if op.operands.len() >= 3 => {
1999 if let (Some(r), Some(g), Some(b)) = (
2000 op.operands.first().and_then(as_number),
2001 op.operands.get(1).and_then(as_number),
2002 op.operands.get(2).and_then(as_number),
2003 ) {
2004 state.fill_color = Some([
2005 clamp_unit_to_u8(r),
2006 clamp_unit_to_u8(g),
2007 clamp_unit_to_u8(b),
2008 255,
2009 ]);
2010 }
2011 }
2012 "g" => {
2013 if let Some(v) = op.operands.first().and_then(as_number) {
2014 let byte = clamp_unit_to_u8(v);
2015 state.fill_color = Some([byte, byte, byte, 255]);
2016 }
2017 }
2018 "k" if op.operands.len() >= 4 => {
2019 if let (Some(c), Some(m), Some(y), Some(kk)) = (
2020 op.operands.first().and_then(as_number),
2021 op.operands.get(1).and_then(as_number),
2022 op.operands.get(2).and_then(as_number),
2023 op.operands.get(3).and_then(as_number),
2024 ) {
2025 state.fill_color = Some(cmyk_to_rgba(c, m, y, kk));
2026 }
2027 }
2028 "RG" | "G" | "K" => {}
2029 "sc" | "scn" => {
2030 state.fill_color = match op.operands.len() {
2031 1 => op.operands.first().and_then(as_number).map(|v| {
2032 let b = clamp_unit_to_u8(v);
2033 [b, b, b, 255]
2034 }),
2035 3 => {
2036 let r = op.operands.first().and_then(as_number);
2037 let g = op.operands.get(1).and_then(as_number);
2038 let b = op.operands.get(2).and_then(as_number);
2039 match (r, g, b) {
2040 (Some(r), Some(g), Some(b)) => Some([
2041 clamp_unit_to_u8(r),
2042 clamp_unit_to_u8(g),
2043 clamp_unit_to_u8(b),
2044 255,
2045 ]),
2046 _ => None,
2047 }
2048 }
2049 4 => {
2050 let c = op.operands.first().and_then(as_number);
2051 let m = op.operands.get(1).and_then(as_number);
2052 let y = op.operands.get(2).and_then(as_number);
2053 let kk = op.operands.get(3).and_then(as_number);
2054 match (c, m, y, kk) {
2055 (Some(c), Some(m), Some(y), Some(kk)) => {
2056 Some(cmyk_to_rgba(c, m, y, kk))
2057 }
2058 _ => None,
2059 }
2060 }
2061 _ => None,
2062 };
2063 }
2064 "cs" | "CS" => {
2065 state.fill_color = None;
2066 }
2067 "BT" => {
2068 state.tm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2069 state.tlm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2070 }
2071 "Tf" if op.operands.len() >= 2 => {
2072 if let Object::Name(ref name) = op.operands[0] {
2073 state.font_name = String::from_utf8_lossy(name).to_string();
2074 }
2075 if let Some(size) = as_number(&op.operands[1]) {
2076 state.font_size = size;
2077 }
2078 }
2079 "Tc" => {
2080 if let Some(v) = op.operands.first().and_then(as_number) {
2081 state.tc = v;
2082 }
2083 }
2084 "Tw" => {
2085 if let Some(v) = op.operands.first().and_then(as_number) {
2086 state.tw = v;
2087 }
2088 }
2089 "Tz" => {
2090 if let Some(v) = op.operands.first().and_then(as_number) {
2091 state.th = v;
2092 }
2093 }
2094 "TL" => {
2095 if let Some(v) = op.operands.first().and_then(as_number) {
2096 state.tl = v;
2097 }
2098 }
2099 "Ts" => {
2100 if let Some(v) = op.operands.first().and_then(as_number) {
2101 state.ts = v;
2102 }
2103 }
2104 "Td" if op.operands.len() >= 2 => {
2105 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2106 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2107 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, tx, ty]);
2108 state.tlm = new_tlm;
2109 state.tm = new_tlm;
2110 }
2111 "TD" if op.operands.len() >= 2 => {
2112 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2113 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2114 state.tl = -ty;
2115 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, tx, ty]);
2116 state.tlm = new_tlm;
2117 state.tm = new_tlm;
2118 }
2119 "Tm" => {
2120 if let Some(m) = extract_matrix(&op.operands) {
2121 state.tm = m;
2122 state.tlm = m;
2123 }
2124 }
2125 "T*" => {
2126 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, 0.0, -state.tl]);
2127 state.tlm = new_tlm;
2128 state.tm = new_tlm;
2129 }
2130 "Tj" => {
2131 let fi = font_map.get(&state.font_name);
2132 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2133 let raw = op.operands.iter().find_map(|o| {
2134 if let Object::String(b, _) = o {
2135 Some(b.as_slice())
2136 } else {
2137 None
2138 }
2139 });
2140
2141 let x = state.tm[4];
2143 let y = state.tm[5];
2144 let gctx = GlyphCtx {
2145 font_size: state.font_size,
2146 tc: state.tc,
2147 tw: state.tw,
2148 th: state.th,
2149 };
2150 let (char_bounds, width_source, total_adv) = match (raw, fi) {
2151 (Some(raw), Some(fi)) => {
2152 let (cb, ws, adv) = char_bounds_from_bytes(raw, fi, x, y, &gctx);
2153 (cb, ws, adv)
2154 }
2155 _ => {
2156 let cw = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2157 let n = text.glyph_count();
2158 let cb = (0..n)
2159 .map(|i| {
2160 let cx = x + i as f64 * cw;
2161 [cx, y, cx + cw, y + state.font_size]
2162 })
2163 .collect();
2164 (cb, WidthSource::Estimate, n as f64 * cw)
2165 }
2166 };
2167
2168 if !text.is_empty() {
2171 let display_text = maybe_decompose_decoded(&text);
2172 blocks.push(TextBlock {
2173 text: display_text,
2174 page,
2175 bbox: [x, y, x + total_adv, y + state.font_size],
2176 font_name: state.font_name.clone(),
2177 font_size: state.font_size,
2178 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2179 base_font: font_map
2180 .get(&state.font_name)
2181 .and_then(|fi| fi.base_font.clone()),
2182 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2183 is_italic: font_map
2184 .get(&state.font_name)
2185 .is_some_and(|fi| fi.is_italic),
2186 color: state.fill_color,
2187 width_source,
2188 char_bounds,
2189 });
2190 }
2191
2192 state.tm[4] = x + total_adv;
2193 }
2194 }
2195 "TJ" => {
2196 if let Some(Object::Array(ref arr)) = op.operands.first() {
2197 let x_start = state.tm[4];
2198 let y = state.tm[5];
2199 let fi = font_map.get(&state.font_name);
2200
2201 let gctx = GlyphCtx {
2203 font_size: state.font_size,
2204 tc: state.tc,
2205 tw: state.tw,
2206 th: state.th,
2207 };
2208 let (char_bounds, width_source, x_end) = match fi {
2209 Some(fi) => char_bounds_from_tj_array(arr, fi, x_start, y, &gctx),
2210 None => {
2211 let cw = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2212 let mut cx = x_start;
2214 let mut cb = Vec::new();
2215 for item in arr {
2216 if let Object::String(bytes, _) = item {
2217 let n = if fi.map(|f| f.is_cid).unwrap_or(false) {
2218 bytes.len() / 2
2219 } else {
2220 bytes.len()
2221 };
2222 for _ in 0..n {
2223 cb.push([cx, y, cx + cw, y + state.font_size]);
2224 cx += cw + state.tc;
2225 }
2226 } else if let Some(adj) = as_number(item) {
2227 cx -= adj / 1000.0 * state.font_size;
2228 }
2229 }
2230 (cb, WidthSource::Estimate, cx)
2231 }
2232 };
2233
2234 let mut combined_text = DecodedPdfString::default();
2236 for item in arr {
2237 if let Object::String(bytes, _) = item {
2238 combined_text.extend(decode_pdf_string_with_font_marked(bytes, fi));
2239 }
2240 }
2241
2242 state.tm[4] = x_end;
2243
2244 if !combined_text.is_empty() {
2245 blocks.push(TextBlock {
2246 text: maybe_decompose_decoded(&combined_text),
2247 page,
2248 bbox: [x_start, y, x_end, y + state.font_size],
2249 font_name: state.font_name.clone(),
2250 font_size: state.font_size,
2251 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2252 base_font: font_map
2253 .get(&state.font_name)
2254 .and_then(|fi| fi.base_font.clone()),
2255 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2256 is_italic: font_map
2257 .get(&state.font_name)
2258 .is_some_and(|fi| fi.is_italic),
2259 color: state.fill_color,
2260 width_source,
2261 char_bounds,
2262 });
2263 }
2264 }
2265 }
2266 "'" => {
2267 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, 0.0, -state.tl]);
2269 state.tlm = new_tlm;
2270 state.tm = new_tlm;
2271
2272 let fi = font_map.get(&state.font_name);
2273 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2274 let raw = op.operands.iter().find_map(|o| {
2275 if let Object::String(b, _) = o {
2276 Some(b.as_slice())
2277 } else {
2278 None
2279 }
2280 });
2281 let x = state.tm[4];
2282 let y = state.tm[5];
2283 let gctx = GlyphCtx {
2284 font_size: state.font_size,
2285 tc: state.tc,
2286 tw: state.tw,
2287 th: state.th,
2288 };
2289 let (char_bounds, width_source, total_adv) = match (raw, fi) {
2290 (Some(raw), Some(fi)) => {
2291 let (cb, ws, adv) = char_bounds_from_bytes(raw, fi, x, y, &gctx);
2292 (cb, ws, adv)
2293 }
2294 _ => {
2295 let cw = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2296 let n = text.glyph_count();
2297 let cb = (0..n)
2298 .map(|i| {
2299 let cx = x + i as f64 * cw;
2300 [cx, y, cx + cw, y + state.font_size]
2301 })
2302 .collect();
2303 (cb, WidthSource::Estimate, n as f64 * cw)
2304 }
2305 };
2306
2307 if !text.is_empty() {
2308 let display_text = maybe_decompose_decoded(&text);
2309 blocks.push(TextBlock {
2310 text: display_text,
2311 page,
2312 bbox: [x, y, x + total_adv, y + state.font_size],
2313 font_name: state.font_name.clone(),
2314 font_size: state.font_size,
2315 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2316 base_font: font_map
2317 .get(&state.font_name)
2318 .and_then(|fi| fi.base_font.clone()),
2319 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2320 is_italic: font_map
2321 .get(&state.font_name)
2322 .is_some_and(|fi| fi.is_italic),
2323 color: state.fill_color,
2324 width_source,
2325 char_bounds,
2326 });
2327 }
2328
2329 state.tm[4] = x + total_adv;
2330 }
2331 }
2332 "\"" if op.operands.len() >= 3 => {
2333 if let Some(tw) = as_number(&op.operands[0]) {
2335 state.tw = tw;
2336 }
2337 if let Some(tc) = as_number(&op.operands[1]) {
2338 state.tc = tc;
2339 }
2340
2341 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, 0.0, -state.tl]);
2342 state.tlm = new_tlm;
2343 state.tm = new_tlm;
2344
2345 let fi = font_map.get(&state.font_name);
2346 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands[2..], fi)
2347 {
2348 let raw = op.operands[2..].iter().find_map(|o| {
2349 if let Object::String(b, _) = o {
2350 Some(b.as_slice())
2351 } else {
2352 None
2353 }
2354 });
2355 let x = state.tm[4];
2356 let y = state.tm[5];
2357 let gctx = GlyphCtx {
2358 font_size: state.font_size,
2359 tc: state.tc,
2360 tw: state.tw,
2361 th: state.th,
2362 };
2363 let (char_bounds, width_source, total_adv) = match (raw, fi) {
2364 (Some(raw), Some(fi)) => {
2365 let (cb, ws, adv) = char_bounds_from_bytes(raw, fi, x, y, &gctx);
2366 (cb, ws, adv)
2367 }
2368 _ => {
2369 let cw = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2370 let n = text.glyph_count();
2371 let cb = (0..n)
2372 .map(|i| {
2373 let cx = x + i as f64 * cw;
2374 [cx, y, cx + cw, y + state.font_size]
2375 })
2376 .collect();
2377 (cb, WidthSource::Estimate, n as f64 * cw)
2378 }
2379 };
2380
2381 if !text.is_empty() {
2382 let display_text = maybe_decompose_decoded(&text);
2383 blocks.push(TextBlock {
2384 text: display_text,
2385 page,
2386 bbox: [x, y, x + total_adv, y + state.font_size],
2387 font_name: state.font_name.clone(),
2388 font_size: state.font_size,
2389 actual_text: current_actual_text(&mc_stack, inherited_actual_text),
2390 base_font: font_map
2391 .get(&state.font_name)
2392 .and_then(|fi| fi.base_font.clone()),
2393 is_bold: font_map.get(&state.font_name).is_some_and(|fi| fi.is_bold),
2394 is_italic: font_map
2395 .get(&state.font_name)
2396 .is_some_and(|fi| fi.is_italic),
2397 color: state.fill_color,
2398 width_source,
2399 char_bounds,
2400 });
2401 }
2402
2403 state.tm[4] = x + total_adv;
2404 }
2405 }
2406 "BMC" => {
2407 mc_stack.push(MarkedContentEntry::default());
2411 }
2412 "BDC" => {
2413 let actual_text = op
2417 .operands
2418 .get(1)
2419 .and_then(|o| resolve_bdc_properties(doc_and_resources, o))
2420 .as_ref()
2421 .and_then(extract_actual_text);
2422 mc_stack.push(MarkedContentEntry { actual_text });
2423 }
2424 "EMC" => {
2425 mc_stack.pop();
2428 }
2429 "Do" if depth < 5 => {
2430 if let Some((doc, resources)) = doc_and_resources {
2435 if let Some(Object::Name(ref xobj_name)) = op.operands.first() {
2436 let xobj_name_str = String::from_utf8_lossy(xobj_name);
2437 let inherited_for_child =
2438 current_actual_text(&mc_stack, inherited_actual_text);
2439 if let Some(xobj_blocks) = extract_form_xobject_text(
2440 doc,
2441 resources,
2442 &xobj_name_str,
2443 page,
2444 font_map,
2445 depth,
2446 inherited_for_child.as_deref(),
2447 ) {
2448 blocks.extend(xobj_blocks);
2449 }
2450 }
2451 }
2452 }
2453 _ => {}
2454 }
2455 }
2456
2457 blocks
2458}
2459
2460fn extract_form_xobject_text(
2467 doc: &Document,
2468 resources: &lopdf::Dictionary,
2469 name: &str,
2470 page: u32,
2471 font_map: &HashMap<String, FontInfo>,
2472 depth: u32,
2473 inherited_actual_text: Option<&str>,
2474) -> Option<Vec<TextBlock>> {
2475 let xobj_dict = match resources.get(b"XObject").ok()? {
2477 Object::Dictionary(d) => d.clone(),
2478 Object::Reference(r) => match doc.get_object(*r).ok()? {
2479 Object::Dictionary(d) => d.clone(),
2480 _ => return None,
2481 },
2482 _ => return None,
2483 };
2484
2485 let xobj_ref = match xobj_dict.get(name.as_bytes()).ok()? {
2486 Object::Reference(r) => *r,
2487 _ => return None,
2488 };
2489
2490 let stream = match doc.get_object(xobj_ref).ok()? {
2491 Object::Stream(s) => s.clone(),
2492 _ => return None,
2493 };
2494
2495 let subtype = stream
2497 .dict
2498 .get(b"Subtype")
2499 .ok()
2500 .and_then(|o| match o {
2501 Object::Name(n) => Some(String::from_utf8_lossy(n).to_string()),
2502 _ => None,
2503 })
2504 .unwrap_or_default();
2505 if subtype != "Form" {
2506 return None;
2507 }
2508
2509 let content_bytes = stream
2511 .decompressed_content()
2512 .ok()
2513 .unwrap_or_else(|| stream.content.clone());
2514 let content = Content::decode(&content_bytes).ok()?;
2515
2516 let mut xobj_font_map = font_map.clone();
2519 if let Some(xobj_resources) = resolve_dict(doc, &stream.dict, b"Resources") {
2520 if let Some(xobj_fonts) = resolve_dict(doc, &xobj_resources, b"Font") {
2521 for (name_bytes, value) in xobj_fonts.iter() {
2522 let fname = String::from_utf8_lossy(name_bytes).to_string();
2523 if let Some(fi) = build_font_info_from_value(doc, value) {
2524 xobj_font_map.insert(fname, fi);
2525 }
2526 }
2527 }
2528 }
2529
2530 let xobj_resources =
2532 resolve_dict(doc, &stream.dict, b"Resources").unwrap_or_else(|| resources.clone());
2533
2534 Some(extract_blocks_from_ops_inner(
2535 &content.operations,
2536 page,
2537 &xobj_font_map,
2538 Some((doc, &xobj_resources)),
2539 depth + 1,
2540 inherited_actual_text,
2541 ))
2542}
2543
2544fn build_font_info_from_value(doc: &Document, value: &Object) -> Option<FontInfo> {
2546 let font = match value {
2547 Object::Reference(r) => match doc.get_object(*r).ok()? {
2548 Object::Dictionary(d) => d.clone(),
2549 _ => return None,
2550 },
2551 Object::Dictionary(d) => d.clone(),
2552 _ => return None,
2553 };
2554
2555 let subtype = font
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
2564 let is_cid = subtype == "Type0";
2565 let mut to_unicode = parse_to_unicode_from_font(doc, &font);
2566
2567 if to_unicode.is_empty() && is_cid {
2568 if let Ok(Object::Array(descendants)) = font.get(b"DescendantFonts") {
2569 for d in descendants {
2570 let desc_dict = match d {
2571 Object::Reference(r) => {
2572 let Some(Object::Dictionary(d)) = doc.get_object(*r).ok() else {
2575 continue;
2576 };
2577 d
2578 }
2579 Object::Dictionary(d) => d,
2580 _ => continue,
2581 };
2582 let tu = parse_to_unicode_from_font(doc, desc_dict);
2583 if !tu.is_empty() {
2584 to_unicode = tu;
2585 break;
2586 }
2587 }
2588 }
2589 }
2590
2591 let (encoding_map, ct_codes) = if !is_cid {
2592 build_encoding_map(doc, &font)
2593 } else {
2594 ([None; 256], [false; 256])
2595 };
2596
2597 let (base_font, is_bold, is_italic) = derive_font_style(doc, &font);
2598 let (simple_widths, cid_default_width, cid_widths) = parse_font_widths(doc, &font, is_cid);
2599
2600 Some(FontInfo {
2601 is_cid,
2602 to_unicode,
2603 encoding_map,
2604 ct_codes,
2605 base_font,
2606 is_bold,
2607 is_italic,
2608 simple_widths,
2609 cid_default_width,
2610 cid_widths,
2611 })
2612}
2613
2614fn push_glyph_positioned(
2628 chars: &mut Vec<PositionedChar>,
2629 state: &mut TextState,
2630 page: u32,
2631 glyph: char,
2632 is_ct_origin: bool,
2633 char_w: f64,
2634) {
2635 let (gx, gy) = apply_ctm(state);
2636 let constituents: Option<String> = if LIGATURE_DECOMP {
2637 if let Some(s) = decompose_ligature_char_with_origin(glyph, is_ct_origin) {
2641 Some(s.to_string())
2642 } else {
2643 decompose_glyph_to_string(glyph)
2644 }
2645 } else {
2646 None
2647 };
2648 match constituents {
2649 None => {
2650 chars.push(PositionedChar {
2651 ch: glyph,
2652 page,
2653 bbox: [gx, gy, gx + char_w, gy + state.font_size],
2654 });
2655 }
2656 Some(s) => {
2657 let n = s.chars().count() as f64;
2658 let part_w = char_w / n;
2659 for (i, c) in s.chars().enumerate() {
2660 let x = gx + part_w * i as f64;
2661 chars.push(PositionedChar {
2662 ch: c,
2663 page,
2664 bbox: [x, gy, x + part_w, gy + state.font_size],
2665 });
2666 }
2667 }
2668 }
2669 state.tm[4] += char_w + state.tc;
2670}
2671
2672fn extract_chars_from_ops(
2674 ops: &[Operation],
2675 page: u32,
2676 font_map: &HashMap<String, FontInfo>,
2677) -> Vec<PositionedChar> {
2678 let mut state = TextState::default();
2679 let mut chars = Vec::new();
2680
2681 for op in ops {
2682 match op.operator.as_str() {
2683 "q" => {
2684 state.gs_stack.push(GraphicsState {
2685 ctm: state.ctm,
2686 fill_color: state.fill_color,
2687 });
2688 }
2689 "Q" => {
2690 if let Some(gs) = state.gs_stack.pop() {
2691 state.ctm = gs.ctm;
2692 state.fill_color = gs.fill_color;
2693 }
2694 }
2695 "cm" => {
2696 if let Some(m) = extract_matrix(&op.operands) {
2697 state.ctm = multiply_matrix(&state.ctm, &m);
2698 }
2699 }
2700 "rg" if op.operands.len() >= 3 => {
2707 if let (Some(r), Some(g), Some(b)) = (
2708 op.operands.first().and_then(as_number),
2709 op.operands.get(1).and_then(as_number),
2710 op.operands.get(2).and_then(as_number),
2711 ) {
2712 state.fill_color = Some([
2713 clamp_unit_to_u8(r),
2714 clamp_unit_to_u8(g),
2715 clamp_unit_to_u8(b),
2716 255,
2717 ]);
2718 }
2719 }
2720 "g" => {
2721 if let Some(v) = op.operands.first().and_then(as_number) {
2722 let byte = clamp_unit_to_u8(v);
2723 state.fill_color = Some([byte, byte, byte, 255]);
2724 }
2725 }
2726 "k" if op.operands.len() >= 4 => {
2727 if let (Some(c), Some(m), Some(y), Some(kk)) = (
2728 op.operands.first().and_then(as_number),
2729 op.operands.get(1).and_then(as_number),
2730 op.operands.get(2).and_then(as_number),
2731 op.operands.get(3).and_then(as_number),
2732 ) {
2733 state.fill_color = Some(cmyk_to_rgba(c, m, y, kk));
2734 }
2735 }
2736 "RG" | "G" | "K" => {}
2737 "sc" | "scn" => {
2738 state.fill_color = match op.operands.len() {
2739 1 => op.operands.first().and_then(as_number).map(|v| {
2740 let b = clamp_unit_to_u8(v);
2741 [b, b, b, 255]
2742 }),
2743 3 => {
2744 let r = op.operands.first().and_then(as_number);
2745 let g = op.operands.get(1).and_then(as_number);
2746 let b = op.operands.get(2).and_then(as_number);
2747 match (r, g, b) {
2748 (Some(r), Some(g), Some(b)) => Some([
2749 clamp_unit_to_u8(r),
2750 clamp_unit_to_u8(g),
2751 clamp_unit_to_u8(b),
2752 255,
2753 ]),
2754 _ => None,
2755 }
2756 }
2757 4 => {
2758 let c = op.operands.first().and_then(as_number);
2759 let m = op.operands.get(1).and_then(as_number);
2760 let y = op.operands.get(2).and_then(as_number);
2761 let kk = op.operands.get(3).and_then(as_number);
2762 match (c, m, y, kk) {
2763 (Some(c), Some(m), Some(y), Some(kk)) => {
2764 Some(cmyk_to_rgba(c, m, y, kk))
2765 }
2766 _ => None,
2767 }
2768 }
2769 _ => None,
2770 };
2771 }
2772 "cs" | "CS" => {
2773 state.fill_color = None;
2774 }
2775 "BT" => {
2776 state.tm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2777 state.tlm = [1.0, 0.0, 0.0, 1.0, 0.0, 0.0];
2778 }
2779 "Tf" if op.operands.len() >= 2 => {
2780 if let Object::Name(ref name) = op.operands[0] {
2781 state.font_name = String::from_utf8_lossy(name).to_string();
2782 }
2783 if let Some(size) = as_number(&op.operands[1]) {
2784 state.font_size = size;
2785 }
2786 }
2787 "Tc" => {
2788 if let Some(v) = op.operands.first().and_then(as_number) {
2789 state.tc = v;
2790 }
2791 }
2792 "Tw" => {
2793 if let Some(v) = op.operands.first().and_then(as_number) {
2794 state.tw = v;
2795 }
2796 }
2797 "Tz" => {
2798 if let Some(v) = op.operands.first().and_then(as_number) {
2799 state.th = v;
2800 }
2801 }
2802 "TL" => {
2803 if let Some(v) = op.operands.first().and_then(as_number) {
2804 state.tl = v;
2805 }
2806 }
2807 "Ts" => {
2808 if let Some(v) = op.operands.first().and_then(as_number) {
2809 state.ts = v;
2810 }
2811 }
2812 "Td" if op.operands.len() >= 2 => {
2813 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2814 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2815 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, tx, ty]);
2816 state.tlm = new_tlm;
2817 state.tm = new_tlm;
2818 }
2819 "TD" if op.operands.len() >= 2 => {
2820 let tx = as_number(&op.operands[0]).unwrap_or(0.0);
2821 let ty = as_number(&op.operands[1]).unwrap_or(0.0);
2822 state.tl = -ty;
2823 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, tx, ty]);
2824 state.tlm = new_tlm;
2825 state.tm = new_tlm;
2826 }
2827 "Tm" => {
2828 if let Some(m) = extract_matrix(&op.operands) {
2829 state.tm = m;
2830 state.tlm = m;
2831 }
2832 }
2833 "T*" => {
2834 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, 0.0, -state.tl]);
2835 state.tlm = new_tlm;
2836 state.tm = new_tlm;
2837 }
2838 "Tj" => {
2839 let fi = font_map.get(&state.font_name);
2840 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2841 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2842 for (ch, is_ct_origin) in text.iter() {
2843 push_glyph_positioned(
2844 &mut chars,
2845 &mut state,
2846 page,
2847 ch,
2848 is_ct_origin,
2849 char_w,
2850 );
2851 }
2852 }
2853 }
2854 "TJ" => {
2855 if let Some(Object::Array(ref arr)) = op.operands.first() {
2856 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2857 let fi = font_map.get(&state.font_name);
2858 for item in arr {
2859 match item {
2860 Object::String(bytes, _) => {
2861 let text = decode_pdf_string_with_font_marked(bytes, fi);
2862 for (ch, is_ct_origin) in text.iter() {
2863 push_glyph_positioned(
2864 &mut chars,
2865 &mut state,
2866 page,
2867 ch,
2868 is_ct_origin,
2869 char_w,
2870 );
2871 }
2872 }
2873 _ => {
2874 if let Some(adj) = as_number(item) {
2875 state.tm[4] -= adj / 1000.0 * state.font_size;
2876 }
2877 }
2878 }
2879 }
2880 }
2881 }
2882 "'" => {
2883 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, 0.0, -state.tl]);
2884 state.tlm = new_tlm;
2885 state.tm = new_tlm;
2886
2887 let fi = font_map.get(&state.font_name);
2888 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands, fi) {
2889 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2890 for (ch, is_ct_origin) in text.iter() {
2891 push_glyph_positioned(
2892 &mut chars,
2893 &mut state,
2894 page,
2895 ch,
2896 is_ct_origin,
2897 char_w,
2898 );
2899 }
2900 }
2901 }
2902 "\"" if op.operands.len() >= 3 => {
2903 if let Some(tw) = as_number(&op.operands[0]) {
2904 state.tw = tw;
2905 }
2906 if let Some(tc) = as_number(&op.operands[1]) {
2907 state.tc = tc;
2908 }
2909
2910 let new_tlm = multiply_matrix(&state.tlm, &[1.0, 0.0, 0.0, 1.0, 0.0, -state.tl]);
2911 state.tlm = new_tlm;
2912 state.tm = new_tlm;
2913
2914 let fi = font_map.get(&state.font_name);
2915 if let Some(text) = extract_decoded_string_operand_with_font(&op.operands[2..], fi)
2916 {
2917 let char_w = state.font_size * APPROX_CHAR_WIDTH * (state.th / 100.0);
2918 for (ch, is_ct_origin) in text.iter() {
2919 push_glyph_positioned(
2920 &mut chars,
2921 &mut state,
2922 page,
2923 ch,
2924 is_ct_origin,
2925 char_w,
2926 );
2927 }
2928 }
2929 }
2930 _ => {}
2931 }
2932 }
2933
2934 chars
2935}
2936
2937#[inline]
2946fn apply_ctm(state: &TextState) -> (f64, f64) {
2947 let x = state.ctm[0] * state.tm[4] + state.ctm[2] * state.tm[5] + state.ctm[4];
2948 let y = state.ctm[1] * state.tm[4] + state.ctm[3] * state.tm[5] + state.ctm[5];
2949 (x, y)
2950}
2951
2952fn extract_decoded_string_operand_with_font(
2954 operands: &[Object],
2955 font_info: Option<&FontInfo>,
2956) -> Option<DecodedPdfString> {
2957 for op in operands {
2958 if let Object::String(bytes, _) = op {
2959 return Some(decode_pdf_string_with_font_marked(bytes, font_info));
2960 }
2961 }
2962 None
2963}
2964
2965fn as_number(obj: &Object) -> Option<f64> {
2967 match obj {
2968 Object::Integer(i) => Some(*i as f64),
2969 Object::Real(f) => Some(*f as f64),
2970 _ => None,
2971 }
2972}
2973
2974fn extract_matrix(operands: &[Object]) -> Option<[f64; 6]> {
2976 if operands.len() < 6 {
2977 return None;
2978 }
2979 let a = as_number(&operands[0])?;
2980 let b = as_number(&operands[1])?;
2981 let c = as_number(&operands[2])?;
2982 let d = as_number(&operands[3])?;
2983 let e = as_number(&operands[4])?;
2984 let f = as_number(&operands[5])?;
2985 Some([a, b, c, d, e, f])
2986}
2987
2988fn multiply_matrix(m1: &[f64; 6], m2: &[f64; 6]) -> [f64; 6] {
2990 [
2991 m1[0] * m2[0] + m1[1] * m2[2],
2992 m1[0] * m2[1] + m1[1] * m2[3],
2993 m1[2] * m2[0] + m1[3] * m2[2],
2994 m1[2] * m2[1] + m1[3] * m2[3],
2995 m1[4] * m2[0] + m1[5] * m2[2] + m2[4],
2996 m1[4] * m2[1] + m1[5] * m2[3] + m2[5],
2997 ]
2998}
2999
3000#[cfg(test)]
3001mod tests {
3002 use super::*;
3003 use lopdf::{dictionary, Document, Object, Stream};
3004
3005 fn make_doc_with_text(content: &[u8]) -> Document {
3007 let mut doc = Document::with_version("1.7");
3008
3009 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3010 let content_id = doc.add_object(Object::Stream(content_stream));
3011
3012 let page_dict = dictionary! {
3013 "Type" => "Page",
3014 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3015 "Contents" => Object::Reference(content_id),
3016 };
3017 let page_id = doc.add_object(Object::Dictionary(page_dict));
3018
3019 let pages_dict = dictionary! {
3020 "Type" => "Pages",
3021 "Kids" => vec![Object::Reference(page_id)],
3022 "Count" => 1_i64,
3023 };
3024 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3025
3026 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3027 d.set("Parent", Object::Reference(pages_id));
3028 }
3029
3030 let catalog = dictionary! {
3031 "Type" => "Catalog",
3032 "Pages" => Object::Reference(pages_id),
3033 };
3034 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3035 doc.trailer.set("Root", Object::Reference(catalog_id));
3036
3037 doc
3038 }
3039
3040 #[test]
3041 fn extract_simple_text() {
3042 let doc = make_doc_with_text(b"BT /F1 12 Tf (Hello World) Tj ET");
3043 let blocks = extract_text(&doc);
3044 assert_eq!(blocks.len(), 1);
3045 assert_eq!(blocks[0].text, "Hello World");
3046 assert_eq!(blocks[0].page, 1);
3047 assert_eq!(blocks[0].font_size, 12.0);
3048 }
3049
3050 #[test]
3051 fn extract_page_text_single() {
3052 let doc = make_doc_with_text(b"BT /F1 12 Tf (Hello) Tj ET");
3053 let text = extract_page_text(&doc, 1).unwrap();
3054 assert_eq!(text, "Hello");
3055 }
3056
3057 #[test]
3058 fn extract_page_text_out_of_range() {
3059 let doc = make_doc_with_text(b"BT /F1 12 Tf (Hello) Tj ET");
3060 let result = extract_page_text(&doc, 5);
3061 assert!(result.is_err());
3062 }
3063
3064 #[test]
3065 fn extract_positioned_chars_basic() {
3066 let doc = make_doc_with_text(b"BT /F1 12 Tf (AB) Tj ET");
3067 let chars = extract_positioned_chars(&doc, 1).unwrap();
3068 assert_eq!(chars.len(), 2);
3069 assert_eq!(chars[0].ch, 'A');
3070 assert_eq!(chars[1].ch, 'B');
3071 assert_eq!(chars[0].page, 1);
3072 assert!(chars[1].bbox[0] > chars[0].bbox[0]);
3074 }
3075
3076 #[test]
3077 fn extract_tj_array() {
3078 let doc = make_doc_with_text(b"BT /F1 12 Tf [(He) -100 (llo)] TJ ET");
3079 let blocks = extract_text(&doc);
3080 assert_eq!(blocks.len(), 1);
3081 assert_eq!(blocks[0].text, "Hello");
3082 }
3083
3084 #[test]
3085 fn empty_page_extracts_no_text() {
3086 let doc = make_doc_with_text(b"q Q");
3087 let blocks = extract_text(&doc);
3088 assert!(blocks.is_empty());
3089 }
3090
3091 #[test]
3092 fn multiline_text_extraction() {
3093 let doc = make_doc_with_text(b"BT /F1 12 Tf 12 TL (Line1) Tj T* (Line2) Tj ET");
3094 let blocks = extract_text(&doc);
3095 assert_eq!(blocks.len(), 2);
3096 assert_eq!(blocks[0].text, "Line1");
3097 assert_eq!(blocks[1].text, "Line2");
3098 }
3099
3100 #[test]
3104 fn actual_text_basic_bdc_override() {
3105 let doc = make_doc_with_text(b"BT /F1 12 Tf /Span <</ActualText (fi)>> BDC (X) Tj EMC ET");
3106 let blocks = extract_text(&doc);
3107 assert_eq!(blocks.len(), 1);
3108 assert_eq!(blocks[0].text, "X");
3109 assert_eq!(blocks[0].actual_text.as_deref(), Some("fi"));
3110 }
3111
3112 #[test]
3116 fn actual_text_nested_bdc_inner_overrides_outer() {
3117 let doc = make_doc_with_text(
3118 b"BT /F1 12 Tf \
3119 /Span <</ActualText (outer)>> BDC \
3120 (A) Tj \
3121 /Span <</ActualText (inner)>> BDC \
3122 (B) Tj \
3123 EMC \
3124 (C) Tj \
3125 EMC ET",
3126 );
3127 let blocks = extract_text(&doc);
3128 assert_eq!(blocks.len(), 3);
3129 assert_eq!(blocks[0].text, "A");
3130 assert_eq!(blocks[0].actual_text.as_deref(), Some("outer"));
3131 assert_eq!(blocks[1].text, "B");
3132 assert_eq!(blocks[1].actual_text.as_deref(), Some("inner"));
3133 assert_eq!(blocks[2].text, "C");
3134 assert_eq!(blocks[2].actual_text.as_deref(), Some("outer"));
3135 }
3136
3137 #[test]
3140 fn actual_text_bmc_does_not_leak_emc() {
3141 let doc = make_doc_with_text(
3142 b"BT /F1 12 Tf \
3143 /Span <</ActualText (X)>> BDC \
3144 /Artifact BMC (in) Tj EMC \
3145 (out) Tj \
3146 EMC \
3147 (after) Tj ET",
3148 );
3149 let blocks = extract_text(&doc);
3150 assert_eq!(blocks.len(), 3);
3151 assert_eq!(blocks[0].actual_text.as_deref(), Some("X"));
3152 assert_eq!(blocks[1].actual_text.as_deref(), Some("X"));
3153 assert_eq!(blocks[2].actual_text, None);
3154 }
3155
3156 fn make_doc_with_xobj_inside_bdc(
3162 page_pre: &[u8],
3163 xobj_content: &[u8],
3164 page_post: &[u8],
3165 actual_text: &str,
3166 ) -> Document {
3167 let mut doc = Document::with_version("1.7");
3168
3169 let xobj_dict = dictionary! {
3170 "Type" => "XObject",
3171 "Subtype" => "Form",
3172 "BBox" => vec![0.into(), 0.into(), 100.into(), 100.into()],
3173 };
3174 let xobj_stream = Stream::new(xobj_dict, xobj_content.to_vec());
3175 let xobj_id = doc.add_object(Object::Stream(xobj_stream));
3176
3177 let mut page_content = Vec::<u8>::new();
3179 page_content.extend_from_slice(b"BT /F1 12 Tf /Span <</ActualText (");
3180 page_content.extend_from_slice(actual_text.as_bytes());
3181 page_content.extend_from_slice(b")>> BDC ");
3182 page_content.extend_from_slice(page_pre);
3183 page_content.extend_from_slice(b" /Fm0 Do ");
3184 page_content.extend_from_slice(page_post);
3185 page_content.extend_from_slice(b" EMC ET");
3186
3187 let content_stream = Stream::new(dictionary! {}, page_content);
3188 let content_id = doc.add_object(Object::Stream(content_stream));
3189
3190 let resources = dictionary! {
3191 "XObject" => dictionary! { "Fm0" => Object::Reference(xobj_id) },
3192 };
3193 let resources_id = doc.add_object(Object::Dictionary(resources));
3194
3195 let page_dict = dictionary! {
3196 "Type" => "Page",
3197 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3198 "Contents" => Object::Reference(content_id),
3199 "Resources" => Object::Reference(resources_id),
3200 };
3201 let page_id = doc.add_object(Object::Dictionary(page_dict));
3202
3203 let pages_dict = dictionary! {
3204 "Type" => "Pages",
3205 "Kids" => vec![Object::Reference(page_id)],
3206 "Count" => 1_i64,
3207 };
3208 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3209
3210 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3211 d.set("Parent", Object::Reference(pages_id));
3212 }
3213
3214 let catalog = dictionary! {
3215 "Type" => "Catalog",
3216 "Pages" => Object::Reference(pages_id),
3217 };
3218 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3219 doc.trailer.set("Root", Object::Reference(catalog_id));
3220
3221 doc
3222 }
3223
3224 #[test]
3231 fn actual_text_propagates_into_form_xobject_recursion() {
3232 let doc = make_doc_with_xobj_inside_bdc(
3233 b"(pre) Tj",
3234 b"BT /F1 12 Tf (inside) Tj ET",
3235 b"(post) Tj",
3236 "wrapped",
3237 );
3238 let blocks = extract_text(&doc);
3239 assert_eq!(blocks.len(), 3);
3240 assert_eq!(blocks[0].text, "pre");
3241 assert_eq!(blocks[0].actual_text.as_deref(), Some("wrapped"));
3242 assert_eq!(blocks[1].text, "inside");
3245 assert_eq!(
3246 blocks[1].actual_text.as_deref(),
3247 Some("wrapped"),
3248 "Form XObject text lost surrounding /ActualText (issue #1358)"
3249 );
3250 assert_eq!(blocks[2].text, "post");
3251 assert_eq!(blocks[2].actual_text.as_deref(), Some("wrapped"));
3252 }
3253
3254 #[test]
3261 fn actual_text_inner_xobj_bdc_overrides_inherited() {
3262 let doc = make_doc_with_xobj_inside_bdc(
3263 b"",
3264 b"BT /F1 12 Tf \
3265 /Span <</ActualText (inner)>> BDC (B) Tj EMC \
3266 (after) Tj ET",
3267 b"",
3268 "outer",
3269 );
3270 let blocks = extract_text(&doc);
3271 assert_eq!(blocks.len(), 2);
3272 assert_eq!(blocks[0].text, "B");
3273 assert_eq!(blocks[0].actual_text.as_deref(), Some("inner"));
3274 assert_eq!(blocks[1].text, "after");
3275 assert_eq!(blocks[1].actual_text.as_deref(), Some("outer"));
3276 }
3277
3278 #[test]
3281 fn actual_text_utf16be_bom_decodes() {
3282 let doc = make_doc_with_text(
3284 b"BT /F1 12 Tf /Span <</ActualText <FEFF00660069> >> BDC (X) Tj EMC ET",
3285 );
3286 let blocks = extract_text(&doc);
3287 assert_eq!(blocks.len(), 1);
3288 assert_eq!(blocks[0].actual_text.as_deref(), Some("fi"));
3289 }
3290
3291 #[test]
3294 fn ligature_ff_decomposes() {
3295 assert_eq!(decompose_ligature_char('\u{FB00}'), Some("ff"));
3296 assert_eq!(decompose_ligatures("\u{FB00}"), "ff");
3297 assert_eq!(decompose_ligatures("o\u{FB00}ice"), "office");
3298 }
3299
3300 #[test]
3301 fn ligature_fi_decomposes() {
3302 assert_eq!(decompose_ligature_char('\u{FB01}'), Some("fi"));
3303 assert_eq!(decompose_ligatures("\u{FB01}"), "fi");
3304 assert_eq!(decompose_ligatures("of\u{FB01}ce"), "office");
3305 }
3306
3307 #[test]
3308 fn ligature_fl_decomposes() {
3309 assert_eq!(decompose_ligature_char('\u{FB02}'), Some("fl"));
3310 assert_eq!(decompose_ligatures("\u{FB02}ame"), "flame");
3311 }
3312
3313 #[test]
3314 fn ligature_ffi_decomposes() {
3315 assert_eq!(decompose_ligature_char('\u{FB03}'), Some("ffi"));
3316 assert_eq!(decompose_ligatures("o\u{FB03}ce"), "office");
3317 }
3318
3319 #[test]
3320 fn ligature_ffl_decomposes() {
3321 assert_eq!(decompose_ligature_char('\u{FB04}'), Some("ffl"));
3322 assert_eq!(decompose_ligatures("ba\u{FB04}e"), "baffle");
3323 }
3324
3325 #[test]
3330 fn ligature_st_decomposes() {
3331 assert_eq!(decompose_ligature_char('\u{FB06}'), Some("st"));
3332 assert_eq!(decompose_ligature_char('\u{FB05}'), Some("st"));
3333 assert_eq!(decompose_ligatures("fa\u{FB06}"), "fast");
3334 assert_eq!(glyph_name_to_unicode("st"), Some('\u{FB06}'));
3336 }
3337
3338 #[test]
3345 fn ligature_ct_via_glyph_name_emits_string() {
3346 assert_eq!(glyph_name_to_unicode("ct"), None);
3347 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <6101> Tj ET", "ct");
3349 let blocks = extract_text(&doc);
3350 assert_eq!(blocks.len(), 1);
3351 assert_eq!(blocks[0].text, "act");
3352 }
3353
3354 #[test]
3359 fn tounicode_pua_e007_is_preserved() {
3360 let doc = make_doc_with_tounicode_cmap(b"BT /F1 12 Tf <01> Tj ET", 0x01, 0xE007);
3361 let blocks = extract_text(&doc);
3362 assert_eq!(blocks.len(), 1);
3363 assert_eq!(blocks[0].text, "\u{E007}");
3364 assert_ne!(blocks[0].text, "ct");
3365 }
3366
3367 #[test]
3371 fn ligature_ct_positioned_chars_advance_as_single_glyph() {
3372 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <610162> Tj ET", "ct");
3374 let chars = extract_positioned_chars(&doc, 1).unwrap();
3375 let extracted: String = chars.iter().map(|c| c.ch).collect();
3376 assert_eq!(extracted, "actb");
3377 assert_eq!(chars.len(), 4);
3378
3379 let char_w = 12.0 * APPROX_CHAR_WIDTH;
3380 assert!((chars[1].bbox[0] - char_w).abs() < 1e-6);
3381 assert!((chars[3].bbox[0] - (2.0 * char_w)).abs() < 1e-6);
3382 }
3383
3384 fn make_doc_with_ligature_font(content: &[u8], glyph_name: &str) -> Document {
3389 let mut doc = Document::with_version("1.7");
3390
3391 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3392 let content_id = doc.add_object(Object::Stream(content_stream));
3393
3394 let encoding = dictionary! {
3395 "Type" => "Encoding",
3396 "BaseEncoding" => "WinAnsiEncoding",
3397 "Differences" => vec![
3398 1_i64.into(),
3399 Object::Name(glyph_name.as_bytes().to_vec()),
3400 ],
3401 };
3402 let encoding_id = doc.add_object(Object::Dictionary(encoding));
3403
3404 let font = dictionary! {
3405 "Type" => "Font",
3406 "Subtype" => "Type1",
3407 "BaseFont" => "Helvetica",
3408 "Encoding" => Object::Reference(encoding_id),
3409 };
3410 let font_id = doc.add_object(Object::Dictionary(font));
3411
3412 let resources = dictionary! {
3413 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3414 };
3415 let resources_id = doc.add_object(Object::Dictionary(resources));
3416
3417 let page_dict = dictionary! {
3418 "Type" => "Page",
3419 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3420 "Contents" => Object::Reference(content_id),
3421 "Resources" => Object::Reference(resources_id),
3422 };
3423 let page_id = doc.add_object(Object::Dictionary(page_dict));
3424
3425 let pages_dict = dictionary! {
3426 "Type" => "Pages",
3427 "Kids" => vec![Object::Reference(page_id)],
3428 "Count" => 1_i64,
3429 };
3430 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3431
3432 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3433 d.set("Parent", Object::Reference(pages_id));
3434 }
3435
3436 let catalog = dictionary! {
3437 "Type" => "Catalog",
3438 "Pages" => Object::Reference(pages_id),
3439 };
3440 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3441 doc.trailer.set("Root", Object::Reference(catalog_id));
3442
3443 doc
3444 }
3445
3446 #[test]
3452 fn ligature_office_golden_ffi() {
3453 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <6F016365> Tj ET", "ffi");
3455 let blocks = extract_text(&doc);
3456 assert_eq!(blocks.len(), 1);
3457 assert_eq!(blocks[0].text, "office");
3458 }
3459
3460 fn make_doc_with_tounicode_cmap(content: &[u8], src: u8, dst: u32) -> Document {
3465 let mut doc = Document::with_version("1.7");
3466
3467 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3468 let content_id = doc.add_object(Object::Stream(content_stream));
3469
3470 let cmap_text = format!(
3472 "/CIDInit /ProcSet findresource begin\n\
3473 12 dict begin\n\
3474 begincmap\n\
3475 /CMapType 2 def\n\
3476 1 beginbfchar\n\
3477 <{:02X}> <{:04X}>\n\
3478 endbfchar\n\
3479 endcmap CMapName currentdict /CMap defineresource pop end end",
3480 src, dst
3481 );
3482 let cmap_stream = Stream::new(dictionary! {}, cmap_text.into_bytes());
3483 let cmap_id = doc.add_object(Object::Stream(cmap_stream));
3484
3485 let font = dictionary! {
3486 "Type" => "Font",
3487 "Subtype" => "Type1",
3488 "BaseFont" => "Helvetica",
3489 "Encoding" => "WinAnsiEncoding",
3490 "ToUnicode" => Object::Reference(cmap_id),
3491 };
3492 let font_id = doc.add_object(Object::Dictionary(font));
3493
3494 let resources = dictionary! {
3495 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3496 };
3497 let resources_id = doc.add_object(Object::Dictionary(resources));
3498
3499 let page_dict = dictionary! {
3500 "Type" => "Page",
3501 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3502 "Contents" => Object::Reference(content_id),
3503 "Resources" => Object::Reference(resources_id),
3504 };
3505 let page_id = doc.add_object(Object::Dictionary(page_dict));
3506
3507 let pages_dict = dictionary! {
3508 "Type" => "Pages",
3509 "Kids" => vec![Object::Reference(page_id)],
3510 "Count" => 1_i64,
3511 };
3512 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3513
3514 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3515 d.set("Parent", Object::Reference(pages_id));
3516 }
3517
3518 let catalog = dictionary! {
3519 "Type" => "Catalog",
3520 "Pages" => Object::Reference(pages_id),
3521 };
3522 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3523 doc.trailer.set("Root", Object::Reference(catalog_id));
3524
3525 doc
3526 }
3527
3528 #[test]
3533 fn ligature_positioned_chars_split_bbox() {
3534 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "ffi");
3535 let chars = extract_positioned_chars(&doc, 1).unwrap();
3536 assert_eq!(chars.len(), 3, "ffi → 3 chars");
3537 assert_eq!(chars[0].ch, 'f');
3538 assert_eq!(chars[1].ch, 'f');
3539 assert_eq!(chars[2].ch, 'i');
3540 assert!(chars[0].bbox[2] <= chars[1].bbox[0] + 1e-9);
3542 assert!(chars[1].bbox[2] <= chars[2].bbox[0] + 1e-9);
3543 let total = chars[2].bbox[2] - chars[0].bbox[0];
3545 let expected = 12.0 * APPROX_CHAR_WIDTH;
3546 assert!((total - expected).abs() < 1e-6);
3547 }
3548
3549 #[test]
3559 fn positioned_chars_match_extracted_text_for_armenian_ligature() {
3560 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "uniFB13");
3563 let blocks = extract_text(&doc);
3564 let chars = extract_positioned_chars(&doc, 1).unwrap();
3565 assert_eq!(blocks.len(), 1);
3566 assert_eq!(
3568 blocks[0].text.chars().count(),
3569 chars.len(),
3570 "PositionedChar count drifted from extract_text() char count \
3571 (issue #1359 problem 1): blocks[0].text = {:?}, chars = {:?}",
3572 blocks[0].text,
3573 chars.iter().map(|c| c.ch).collect::<String>(),
3574 );
3575 let chars_str: String = chars.iter().map(|c| c.ch).collect();
3577 assert_eq!(blocks[0].text, chars_str);
3578 let total = chars.last().unwrap().bbox[2] - chars.first().unwrap().bbox[0];
3580 let expected = 12.0 * APPROX_CHAR_WIDTH;
3581 assert!((total - expected).abs() < 1e-6);
3582 }
3583
3584 #[test]
3588 fn hebrew_presentation_form_extracts_single_codepoint_nfkd_mapping() {
3589 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "uniFB21");
3591 let blocks = extract_text(&doc);
3592 assert_eq!(blocks.len(), 1);
3593 assert_eq!(blocks[0].text, "\u{05D0}");
3594 assert_eq!(
3595 decompose_glyph_to_string('\u{FB21}').as_deref(),
3596 Some("\u{05D0}")
3597 );
3598 }
3599
3600 #[test]
3604 fn positioned_chars_match_extracted_text_for_hebrew_presentation_form() {
3605 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "uniFB21");
3606 let blocks = extract_text(&doc);
3607 let chars = extract_positioned_chars(&doc, 1).unwrap();
3608 let chars_str: String = chars.iter().map(|c| c.ch).collect();
3609
3610 assert_eq!(blocks.len(), 1);
3611 assert_eq!(blocks[0].text, "\u{05D0}");
3612 assert_eq!(chars_str, blocks[0].text);
3613 assert_eq!(blocks[0].text.chars().count(), chars.len());
3614 }
3615
3616 #[test]
3623 fn tj_block_width_matches_rendered_glyph_count() {
3624 let doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "ffi");
3625 let blocks = extract_text(&doc);
3626 assert_eq!(blocks.len(), 1);
3627 assert_eq!(blocks[0].text, "ffi");
3628 let width = blocks[0].bbox[2] - blocks[0].bbox[0];
3629 let expected = 12.0 * APPROX_CHAR_WIDTH;
3630 assert!(
3631 (width - expected).abs() < 1e-6,
3632 "Tj bbox width {} != one glyph's char_w {} (issue #1359 problem 2)",
3633 width,
3634 expected,
3635 );
3636 }
3637
3638 #[test]
3644 fn tj_and_tj_array_bbox_widths_agree_for_ligature() {
3645 let tj_doc = make_doc_with_ligature_font(b"BT /F1 12 Tf <01> Tj ET", "ffi");
3646 let tj_blocks = extract_text(&tj_doc);
3647 let tj_doc_arr = make_doc_with_ligature_font(b"BT /F1 12 Tf [<01>] TJ ET", "ffi");
3648 let tj_arr_blocks = extract_text(&tj_doc_arr);
3649 assert_eq!(tj_blocks.len(), 1);
3650 assert_eq!(tj_arr_blocks.len(), 1);
3651 let tj_w = tj_blocks[0].bbox[2] - tj_blocks[0].bbox[0];
3652 let tj_arr_w = tj_arr_blocks[0].bbox[2] - tj_arr_blocks[0].bbox[0];
3653 assert!(
3654 (tj_w - tj_arr_w).abs() < 1e-6,
3655 "Tj bbox width {} disagrees with TJ bbox width {} for the same \
3656 single-glyph ligature input (issue #1359 problem 2)",
3657 tj_w,
3658 tj_arr_w,
3659 );
3660 }
3661
3662 fn make_doc_with_font(content: &[u8], base_font: &str, desc_flags: Option<u32>) -> Document {
3667 let mut doc = Document::with_version("1.7");
3668
3669 let mut font_dict = dictionary! {
3670 "Type" => "Font",
3671 "Subtype" => "Type1",
3672 "BaseFont" => Object::Name(base_font.as_bytes().to_vec()),
3673 };
3674 if let Some(flags) = desc_flags {
3675 let descriptor = dictionary! {
3676 "Type" => "FontDescriptor",
3677 "FontName" => Object::Name(base_font.as_bytes().to_vec()),
3678 "Flags" => Object::Integer(flags as i64),
3679 };
3680 let desc_id = doc.add_object(Object::Dictionary(descriptor));
3681 font_dict.set("FontDescriptor", Object::Reference(desc_id));
3682 }
3683 let font_id = doc.add_object(Object::Dictionary(font_dict));
3684
3685 let resources = dictionary! {
3686 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3687 };
3688 let resources_id = doc.add_object(Object::Dictionary(resources));
3689
3690 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3691 let content_id = doc.add_object(Object::Stream(content_stream));
3692
3693 let page_dict = dictionary! {
3694 "Type" => "Page",
3695 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3696 "Contents" => Object::Reference(content_id),
3697 "Resources" => Object::Reference(resources_id),
3698 };
3699 let page_id = doc.add_object(Object::Dictionary(page_dict));
3700
3701 let pages_dict = dictionary! {
3702 "Type" => "Pages",
3703 "Kids" => vec![Object::Reference(page_id)],
3704 "Count" => 1_i64,
3705 };
3706 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3707
3708 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3709 d.set("Parent", Object::Reference(pages_id));
3710 }
3711
3712 let catalog = dictionary! {
3713 "Type" => "Catalog",
3714 "Pages" => Object::Reference(pages_id),
3715 };
3716 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3717 doc.trailer.set("Root", Object::Reference(catalog_id));
3718
3719 doc
3720 }
3721
3722 #[test]
3723 fn g1_tj_carries_basefont_metadata() {
3724 let doc = make_doc_with_font(b"BT /F1 12 Tf (Hello) Tj ET", "Helvetica", None);
3725 let blocks = extract_text(&doc);
3726 assert_eq!(blocks.len(), 1);
3727 assert_eq!(blocks[0].text, "Hello");
3728 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica"));
3729 assert!(!blocks[0].is_bold);
3730 assert!(!blocks[0].is_italic);
3731 }
3732
3733 #[test]
3734 fn g1_tj_array_carries_basefont_metadata() {
3735 let doc = make_doc_with_font(b"BT /F1 12 Tf [(He) -100 (llo)] TJ ET", "Helvetica", None);
3736 let blocks = extract_text(&doc);
3737 assert_eq!(blocks.len(), 1);
3738 assert_eq!(blocks[0].text, "Hello");
3739 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica"));
3740 }
3741
3742 #[test]
3743 fn g1_bold_inferred_from_basefont_name() {
3744 let doc = make_doc_with_font(b"BT /F1 12 Tf (Bold) Tj ET", "Helvetica-Bold", None);
3745 let blocks = extract_text(&doc);
3746 assert_eq!(blocks.len(), 1);
3747 assert!(blocks[0].is_bold);
3748 assert!(!blocks[0].is_italic);
3749 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica-Bold"));
3750 }
3751
3752 #[test]
3753 fn g1_italic_inferred_from_basefont_name() {
3754 let doc = make_doc_with_font(b"BT /F1 12 Tf (Slanted) Tj ET", "Times-Italic", None);
3755 let blocks = extract_text(&doc);
3756 assert_eq!(blocks.len(), 1);
3757 assert!(blocks[0].is_italic);
3758 assert!(!blocks[0].is_bold);
3759 }
3760
3761 #[test]
3762 fn g1_subset_prefix_stripped_from_basefont() {
3763 let doc = make_doc_with_font(
3764 b"BT /F1 12 Tf (Subset) Tj ET",
3765 "ABCDEF+Helvetica-Bold",
3766 None,
3767 );
3768 let blocks = extract_text(&doc);
3769 assert_eq!(blocks.len(), 1);
3770 assert_eq!(blocks[0].base_font.as_deref(), Some("Helvetica-Bold"));
3771 assert!(blocks[0].is_bold);
3772 }
3773
3774 #[test]
3775 fn g1_italic_inferred_from_descriptor_flag() {
3776 let doc = make_doc_with_font(b"BT /F1 12 Tf (X) Tj ET", "Custom-Roman", Some(0x40));
3778 let blocks = extract_text(&doc);
3779 assert_eq!(blocks.len(), 1);
3780 assert!(blocks[0].is_italic);
3781 assert_eq!(blocks[0].base_font.as_deref(), Some("Custom-Roman"));
3783 }
3784
3785 #[test]
3786 fn g1_rg_color_propagates_to_block() {
3787 let doc = make_doc_with_font(b"1 0 0 rg BT /F1 12 Tf (Red) Tj ET", "Helvetica", None);
3789 let blocks = extract_text(&doc);
3790 assert_eq!(blocks.len(), 1);
3791 assert_eq!(blocks[0].color, Some([255, 0, 0, 255]));
3792 }
3793
3794 #[test]
3795 fn g1_gray_color_via_g_operator() {
3796 let doc = make_doc_with_font(b"0.5 g BT /F1 12 Tf (Gray) Tj ET", "Helvetica", None);
3798 let blocks = extract_text(&doc);
3799 assert_eq!(blocks.len(), 1);
3800 let c = blocks[0].color.expect("expected gray color");
3801 assert_eq!(c[3], 255);
3803 assert_eq!(c[0], c[1]);
3804 assert_eq!(c[1], c[2]);
3805 assert!((c[0] as i32 - 128).abs() <= 1);
3806 }
3807
3808 #[test]
3809 fn g1_default_color_is_black_for_compliant_initial_state() {
3810 let doc = make_doc_with_font(b"BT /F1 12 Tf (Default) Tj ET", "Helvetica", None);
3812 let blocks = extract_text(&doc);
3813 assert_eq!(blocks.len(), 1);
3814 assert_eq!(blocks[0].color, Some([0, 0, 0, 255]));
3815 }
3816
3817 #[test]
3818 fn g1_cmyk_color_via_k_operator() {
3819 let doc = make_doc_with_font(b"1 0 0 0 k BT /F1 12 Tf (Cyan) Tj ET", "Helvetica", None);
3821 let blocks = extract_text(&doc);
3822 assert_eq!(blocks.len(), 1);
3823 let c = blocks[0].color.expect("k should set color");
3824 assert_eq!(c, [0, 255, 255, 255]);
3825 }
3826
3827 #[test]
3828 fn g1_tj_and_tj_array_emit_same_metadata() {
3829 let tj_doc =
3831 make_doc_with_font(b"1 0 0 rg BT /F1 12 Tf (Hi) Tj ET", "Helvetica-Bold", None);
3832 let tj_array_doc = make_doc_with_font(
3833 b"1 0 0 rg BT /F1 12 Tf [(Hi)] TJ ET",
3834 "Helvetica-Bold",
3835 None,
3836 );
3837 let a = extract_text(&tj_doc);
3838 let b = extract_text(&tj_array_doc);
3839 assert_eq!(a.len(), 1);
3840 assert_eq!(b.len(), 1);
3841 assert_eq!(a[0].base_font, b[0].base_font);
3842 assert_eq!(a[0].is_bold, b[0].is_bold);
3843 assert_eq!(a[0].is_italic, b[0].is_italic);
3844 assert_eq!(a[0].color, b[0].color);
3845 }
3846
3847 #[test]
3848 fn g1_color_restored_across_q_capital_q() {
3849 let doc = make_doc_with_font(
3851 b"q 1 0 0 rg Q BT /F1 12 Tf (Outside) Tj ET",
3852 "Helvetica",
3853 None,
3854 );
3855 let blocks = extract_text(&doc);
3856 assert_eq!(blocks.len(), 1);
3857 assert_eq!(blocks[0].color, Some([0, 0, 0, 255]));
3859 }
3860
3861 #[test]
3862 fn g1_no_font_resource_means_no_base_font() {
3863 let doc = make_doc_with_text(b"BT /F1 12 Tf (NoFont) Tj ET");
3866 let blocks = extract_text(&doc);
3867 assert_eq!(blocks.len(), 1);
3868 assert_eq!(blocks[0].base_font, None);
3869 assert!(!blocks[0].is_bold);
3870 assert!(!blocks[0].is_italic);
3871 assert_eq!(blocks[0].color, Some([0, 0, 0, 255]));
3873 assert_eq!(blocks[0].font_name, "F1");
3875 }
3876
3877 #[test]
3878 fn g1_unknown_color_space_marks_color_none() {
3879 let doc = make_doc_with_font(
3881 b"/Cs1 cs 0.5 0.5 0.5 0.5 0.5 scn BT /F1 12 Tf (X) Tj ET",
3882 "Helvetica",
3883 None,
3884 );
3885 let blocks = extract_text(&doc);
3886 assert_eq!(blocks.len(), 1);
3887 assert_eq!(blocks[0].color, None);
3888 }
3889
3890 fn make_doc_with_widths(content: &[u8], first_char: i64, widths: Vec<i64>) -> Document {
3894 let mut doc = Document::with_version("1.7");
3895
3896 let widths_obj = Object::Array(widths.iter().map(|w| Object::Integer(*w)).collect());
3897
3898 let font_dict = dictionary! {
3899 "Type" => "Font",
3900 "Subtype" => "Type1",
3901 "BaseFont" => Object::Name(b"TestFont".to_vec()),
3902 "FirstChar" => Object::Integer(first_char),
3903 "LastChar" => Object::Integer(first_char + widths.len() as i64 - 1),
3904 "Widths" => widths_obj,
3905 };
3906 let font_id = doc.add_object(Object::Dictionary(font_dict));
3907
3908 let resources = dictionary! {
3909 "Font" => dictionary! { "F1" => Object::Reference(font_id) },
3910 };
3911 let resources_id = doc.add_object(Object::Dictionary(resources));
3912
3913 let content_stream = Stream::new(dictionary! {}, content.to_vec());
3914 let content_id = doc.add_object(Object::Stream(content_stream));
3915
3916 let page_dict = dictionary! {
3917 "Type" => "Page",
3918 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
3919 "Contents" => Object::Reference(content_id),
3920 "Resources" => Object::Reference(resources_id),
3921 };
3922 let page_id = doc.add_object(Object::Dictionary(page_dict));
3923
3924 let pages_dict = dictionary! {
3925 "Type" => "Pages",
3926 "Kids" => vec![Object::Reference(page_id)],
3927 "Count" => 1_i64,
3928 };
3929 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
3930
3931 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
3932 d.set("Parent", Object::Reference(pages_id));
3933 }
3934
3935 let catalog = dictionary! {
3936 "Type" => "Catalog",
3937 "Pages" => Object::Reference(pages_id),
3938 };
3939 let catalog_id = doc.add_object(Object::Dictionary(catalog));
3940 doc.trailer.set("Root", Object::Reference(catalog_id));
3941
3942 doc
3943 }
3944
3945 #[test]
3946 fn g2_no_widths_gives_estimate_source() {
3947 let doc = make_doc_with_font(b"BT /F1 10 Tf (A) Tj ET", "Helvetica", None);
3949 let blocks = extract_text(&doc);
3950 assert_eq!(blocks.len(), 1);
3951 assert_eq!(blocks[0].width_source, WidthSource::Estimate);
3952 }
3953
3954 #[test]
3955 fn g2_widths_array_gives_metric_source() {
3956 let doc = make_doc_with_widths(b"BT /F1 10 Tf (A) Tj ET", 65, vec![722]);
3959 let blocks = extract_text(&doc);
3960 assert_eq!(blocks.len(), 1);
3961 assert_eq!(blocks[0].width_source, WidthSource::Metric);
3962 assert_eq!(blocks[0].char_bounds.len(), 1);
3964 let [x0, _y0, x1, _y1] = blocks[0].char_bounds[0];
3965 let advance = x1 - x0;
3967 assert!((advance - 7.22).abs() < 0.01, "advance={advance:.4}");
3968 }
3969
3970 #[test]
3971 fn g2_monospace_uniform_char_bounds() {
3972 let widths: Vec<i64> = vec![600, 600, 600, 600, 600];
3975 let doc = make_doc_with_widths(b"BT /F1 12 Tf (ABCDE) Tj ET", 65, widths);
3976 let blocks = extract_text(&doc);
3977 assert_eq!(blocks.len(), 1);
3978 assert_eq!(blocks[0].width_source, WidthSource::Metric);
3979 assert_eq!(blocks[0].char_bounds.len(), 5);
3980 let expected_adv = 600.0 / 1000.0 * 12.0; for i in 0..5 {
3982 let [x0, _, x1, _] = blocks[0].char_bounds[i];
3983 let adv = x1 - x0;
3984 assert!(
3985 (adv - expected_adv).abs() < 0.01,
3986 "char {i}: advance={adv:.4} expected={expected_adv:.4}"
3987 );
3988 }
3989 }
3990
3991 #[test]
3992 fn g2_proportional_char_bounds_are_contiguous() {
3993 let widths: Vec<i64> = vec![722, 667, 667];
3995 let doc = make_doc_with_widths(b"BT /F1 10 Tf (ABC) Tj ET", 65, widths);
3996 let blocks = extract_text(&doc);
3997 assert_eq!(blocks.len(), 1);
3998 assert_eq!(blocks[0].char_bounds.len(), 3);
3999 for i in 0..2 {
4002 let x1_prev = blocks[0].char_bounds[i][2];
4003 let x0_next = blocks[0].char_bounds[i + 1][0];
4004 assert!(
4005 (x1_prev - x0_next).abs() < 0.001,
4006 "gap between char {i} and {}: {:.4}",
4007 i + 1,
4008 x0_next - x1_prev
4009 );
4010 }
4011 }
4012
4013 #[test]
4014 fn g2_tj_kerning_shifts_subsequent_bounds() {
4015 let widths: Vec<i64> = vec![722, 667];
4022 let doc = make_doc_with_widths(b"BT /F1 10 Tf [(A) -200 (B)] TJ ET", 65, widths);
4023 let blocks = extract_text(&doc);
4024 assert_eq!(blocks.len(), 1);
4025 assert_eq!(blocks[0].char_bounds.len(), 2);
4026 let [x0_a, _, x1_a, _] = blocks[0].char_bounds[0];
4027 let [x0_b, _, _, _] = blocks[0].char_bounds[1];
4028 assert!(
4030 (x1_a - x0_a - 7.22).abs() < 0.01,
4031 "A advance={:.4}",
4032 x1_a - x0_a
4033 );
4034 assert!(x0_b > x1_a, "B should start after A ends");
4036 let gap = x0_b - x1_a;
4037 assert!(
4038 (gap - 2.0).abs() < 0.01,
4039 "kerning gap={gap:.4} expected=2.0"
4040 );
4041 }
4042
4043 #[test]
4044 fn g2_subset_font_prefix_does_not_affect_widths() {
4045 let widths: Vec<i64> = vec![722];
4047 let doc = make_doc_with_widths(b"BT /F1 10 Tf (A) Tj ET", 65, widths);
4048 let blocks = extract_text(&doc);
4049 assert_eq!(blocks.len(), 1);
4050 assert_eq!(blocks[0].width_source, WidthSource::Metric);
4052 assert_eq!(blocks[0].char_bounds.len(), 1);
4053 }
4054
4055 #[test]
4056 fn g2_char_bounds_count_equals_glyph_count_not_unicode_len() {
4057 let doc = make_doc_with_font(b"BT /F1 12 Tf (A) Tj ET", "TestFont", None);
4061 let blocks = extract_text(&doc);
4062 assert_eq!(blocks.len(), 1);
4063 assert_eq!(blocks[0].char_bounds.len(), 1);
4065 }
4066}