1use std::collections::HashMap;
2
3use paperforge_pdf::{ObjectId, PdfObject, StreamDecoder, StreamFilter};
4
5use crate::error::{ExtractError, ExtractResult};
6
7struct FontInfo {
11 two_byte: bool,
12 to_unicode: HashMap<u16, String>,
13}
14
15pub struct Extractor;
16
17impl Extractor {
18 pub fn extract_text(&self, data: &[u8]) -> ExtractResult<String> {
25 let doc = paperforge_pdf::Parser::new()
26 .parse(data)
27 .map_err(|e| ExtractError::Parse(e.to_string()))?;
28
29 let contents = self.page_content_streams(&doc);
30 let mut text = String::new();
31 for (page_index, (content_ids, page_dict)) in contents.iter().enumerate() {
32 if page_index > 0 {
33 text.push('\n');
34 }
35 let fonts = self.page_fonts(&doc, page_dict);
36 for id in content_ids {
37 let Some(obj) = doc.get_object(*id) else {
38 continue;
39 };
40 let Some(stream) = obj.as_stream() else {
41 continue;
42 };
43 if let Ok(bytes) = self.decode(stream) {
44 text.push_str(&extract_text_operators(&bytes, &fonts));
45 }
46 }
47 }
48 Ok(text)
49 }
50
51 fn page_content_streams(
54 &self,
55 doc: &paperforge_pdf::PdfDocument,
56 ) -> Vec<(Vec<ObjectId>, paperforge_pdf::PdfDictionary)> {
57 let mut out = Vec::new();
58 let Some(catalog) = doc.catalog() else {
59 return out;
60 };
61 let Some(PdfObject::Dictionary(catalog_dict)) = doc.get_object(catalog) else {
62 return out;
63 };
64 let Some(PdfObject::Reference(root)) = catalog_dict.get("Pages") else {
65 return out;
66 };
67 let mut stack = vec![*root];
68 while let Some(id) = stack.pop() {
69 let Some(PdfObject::Dictionary(dict)) = doc.get_object(id) else {
70 continue;
71 };
72 match dict.get_name("Type").map(|n| n.as_str()) {
73 Some("Pages") => {
74 if let Some(PdfObject::Array(kids)) = dict.get("Kids") {
75 for kid in kids.0.iter().rev() {
76 if let PdfObject::Reference(r) = kid {
77 stack.push(*r);
78 }
79 }
80 }
81 }
82 Some("Page") => {
83 let ids = self.contents_of(dict);
84 out.push((ids, dict.clone()));
85 }
86 _ => {}
87 }
88 }
89 out
90 }
91
92 fn contents_of(&self, page: &paperforge_pdf::PdfDictionary) -> Vec<ObjectId> {
93 let Some(contents) = page.get("Contents") else {
94 return Vec::new();
95 };
96 match contents {
97 PdfObject::Reference(id) => vec![*id],
98 PdfObject::Array(arr) => arr.0.iter().filter_map(|o| o.as_reference()).collect(),
99 _ => Vec::new(),
100 }
101 }
102
103 fn decode(&self, stream: &paperforge_pdf::PdfStream) -> ExtractResult<Vec<u8>> {
104 let filter = stream.dictionary.get_name("Filter");
105 match filter {
106 Some(f) if f.as_str() == "FlateDecode" || f.as_str() == "Fl" => StreamDecoder::new()
107 .decode_with_limit(&stream.data, StreamFilter::Flate, 100 * 1024 * 1024)
108 .map_err(|e| ExtractError::Parse(e.to_string())),
109 Some(_) => Err(ExtractError::Unsupported(
110 "unsupported content stream filter".into(),
111 )),
112 None => Ok(stream.data.clone()),
113 }
114 }
115
116 fn page_fonts(
120 &self,
121 doc: &paperforge_pdf::PdfDocument,
122 page: &paperforge_pdf::PdfDictionary,
123 ) -> HashMap<String, FontInfo> {
124 let mut fonts = HashMap::new();
125 let mut cur: Option<&paperforge_pdf::PdfDictionary> = Some(page);
126 while let Some(dict) = cur {
127 if let Some(res) = dict.get("Resources") {
128 let res_dict = match res {
129 PdfObject::Dictionary(d) => Some(d),
130 PdfObject::Reference(id) => doc.get_object(*id).and_then(|o| o.as_dict()),
131 _ => None,
132 };
133 if let Some(res_dict) = res_dict {
134 if let Some(PdfObject::Dictionary(font_dict)) = res_dict.get("Font") {
135 for (name, obj) in font_dict.iter() {
136 let font_obj = match obj {
137 PdfObject::Dictionary(d) => Some(d),
138 PdfObject::Reference(id) => {
139 doc.get_object(*id).and_then(|o| o.as_dict())
140 }
141 _ => None,
142 };
143 if let Some(font) = font_obj.and_then(|f| self.font_info(doc, f)) {
144 fonts.insert(name.as_str().to_string(), font);
145 }
146 }
147 }
148 }
149 break; }
151 cur = dict
152 .get("Parent")
153 .and_then(|p| p.as_reference())
154 .and_then(|id| doc.get_object(id).and_then(|o| o.as_dict()));
155 }
156 fonts
157 }
158
159 fn font_info(
160 &self,
161 doc: &paperforge_pdf::PdfDocument,
162 font: &paperforge_pdf::PdfDictionary,
163 ) -> Option<FontInfo> {
164 let to_unicode_ref = font.get("ToUnicode").and_then(|o| o.as_reference())?;
165 let stream = doc.get_object(to_unicode_ref).and_then(|o| o.as_stream())?;
166 let data = self.decode(stream).ok()?;
167 Some(FontInfo {
168 two_byte: font
169 .get_name("Subtype")
170 .is_some_and(|n| n.as_str() == "Type0"),
171 to_unicode: parse_tounicode_cmap(&data),
172 })
173 }
174
175 pub fn extract_metadata(&self, data: &[u8]) -> ExtractResult<Vec<u8>> {
177 let doc = paperforge_pdf::Parser::new()
178 .parse(data)
179 .map_err(|e| ExtractError::Parse(e.to_string()))?;
180
181 let Some(info_id) = doc.info() else {
182 return Ok(Vec::new());
183 };
184 let Some(obj) = doc.get_object(info_id) else {
185 return Ok(Vec::new());
186 };
187 let Some(dict) = obj.as_dict() else {
188 return Ok(Vec::new());
189 };
190 let mut out = String::new();
191 for key in [
192 "Title", "Author", "Subject", "Keywords", "Creator", "Producer",
193 ] {
194 if let Some(bytes) = dict.get_string_bytes(key) {
195 out.push_str(&format!("{key}: {}\n", String::from_utf8_lossy(bytes)));
196 }
197 }
198 Ok(out.into_bytes())
199 }
200}
201
202impl Default for Extractor {
203 fn default() -> Self {
204 Self
205 }
206}
207
208fn extract_text_operators(data: &[u8], fonts: &HashMap<String, FontInfo>) -> String {
215 let mut out = String::new();
216 let mut i = 0usize;
217 let mut bracket_depth = 0usize;
218 let mut current_font: Option<&FontInfo> = None;
219 while i < data.len() {
220 match data[i] {
221 b' ' | b'\t' | b'\r' | b'\n' | 0x0c => i += 1,
222 b'%' => {
223 while i < data.len() && data[i] != b'\n' {
224 i += 1;
225 }
226 }
227 b'[' => {
228 bracket_depth += 1;
229 i += 1;
230 }
231 b']' => {
232 bracket_depth = bracket_depth.saturating_sub(1);
233 i += 1;
234 }
235 b'/' => {
236 let start = i;
238 while i < data.len()
239 && !matches!(data[i], b' ' | b'\t' | b'\r' | b'\n' | 0x0c | b'[' | b'(')
240 {
241 i += 1;
242 }
243 let name = &data[start + 1..i]; let mut j = skip_ws(data, i);
245 if data
247 .get(j)
248 .is_some_and(|b| matches!(b, b'-' | b'+' | b'.' | b'0'..=b'9'))
249 {
250 while data
251 .get(j)
252 .is_some_and(|b| matches!(b, b'-' | b'+' | b'.' | b'0'..=b'9'))
253 {
254 j += 1;
255 }
256 j = skip_ws(data, j);
257 }
258 if data[j..].starts_with(b"Tf") {
259 current_font = fonts.get(std::str::from_utf8(name).unwrap_or_default());
260 }
261 }
262 b'(' => {
263 if let Some(s) = read_literal_string(data, &mut i) {
264 out.push_str(&decode_text_bytes(&s, current_font));
265 if bracket_depth > 0 {
266 out.push(' ');
267 } else if next_operator_is_text_show(data, i) {
268 out.push('\n');
269 }
270 }
271 }
272 b'<' => {
273 if data.get(i + 1) == Some(&b'<') {
274 while i + 1 < data.len() && !(data[i] == b'>' && data[i + 1] == b'>') {
276 i += 1;
277 }
278 i = (i + 2).min(data.len());
279 } else if let Some(s) = read_hex_string(data, &mut i) {
280 out.push_str(&decode_text_bytes(&s, current_font));
281 if bracket_depth > 0 {
282 out.push(' ');
283 } else if next_operator_is_text_show(data, i) {
284 out.push('\n');
285 }
286 }
287 }
288 _ => i += 1,
289 }
290 }
291 out
292}
293
294fn skip_ws(data: &[u8], mut i: usize) -> usize {
295 while data
296 .get(i)
297 .is_some_and(|b| matches!(b, b' ' | b'\t' | b'\r' | b'\n' | 0x0c))
298 {
299 i += 1;
300 }
301 i
302}
303
304fn decode_text_bytes(bytes: &[u8], font: Option<&FontInfo>) -> String {
309 if let Some(font) = font {
310 if !font.to_unicode.is_empty() {
311 let mut out = String::new();
312 let step = if font.two_byte { 2 } else { 1 };
313 for code in bytes.chunks(step) {
314 if code.len() < step {
315 break;
316 }
317 let value = if font.two_byte {
318 u16::from_be_bytes([code[0], code[1]])
319 } else {
320 u16::from(code[0])
321 };
322 match font.to_unicode.get(&value) {
323 Some(s) => out.push_str(s),
324 None => out.push('\u{FFFD}'),
325 }
326 }
327 return out;
328 }
329 }
330 let mut out = String::new();
331 push_text(bytes, &mut out);
332 out
333}
334
335fn next_operator_is_text_show(data: &[u8], mut i: usize) -> bool {
336 while i < data.len() && matches!(data[i], b' ' | b'\t' | b'\r' | b'\n') {
337 i += 1;
338 }
339 data[i..].starts_with(b"Tj") || data[i..].starts_with(b"TJ")
340}
341
342fn parse_tounicode_cmap(data: &[u8]) -> HashMap<u16, String> {
346 let mut map = HashMap::new();
347 let mut i = 0usize;
348 let mut in_bfchar = false;
349 let mut in_bfrange = false;
350 let mut range_array = false;
351 let mut range_lo: u16 = 0;
352 let mut pending_src: Option<Vec<u8>> = None;
353 let mut pending: Vec<Vec<u8>> = Vec::new();
354
355 while i < data.len() {
356 match data[i] {
357 b'a'..=b'z' => {
358 let start = i;
359 while i < data.len() && data[i].is_ascii_alphanumeric() {
360 i += 1;
361 }
362 match &data[start..i] {
363 b"beginbfchar" => {
364 in_bfchar = true;
365 pending_src = None;
366 }
367 b"endbfchar" => {
368 in_bfchar = false;
369 pending_src = None;
370 }
371 b"beginbfrange" => {
372 in_bfrange = true;
373 pending.clear();
374 }
375 b"endbfrange" => {
376 in_bfrange = false;
377 pending.clear();
378 }
379 _ => {}
380 }
381 }
382 b'[' => {
383 if in_bfrange && pending.len() == 2 {
384 range_array = true;
385 range_lo = u16::from_be_bytes([pending[0][0], pending[0][1]]);
386 pending.clear();
387 }
388 i += 1;
389 }
390 b']' => {
391 range_array = false;
392 pending.clear();
393 i += 1;
394 }
395 b'<' => {
396 if let Some(s) = read_hex_string(data, &mut i) {
397 if in_bfchar {
398 match pending_src.take() {
401 Some(src) => insert_tounicode(&mut map, &src, &s),
402 None => pending_src = Some(s),
403 }
404 } else if in_bfrange && range_array {
405 let code = range_lo.wrapping_add(pending.len() as u16);
408 insert_tounicode(&mut map, &code.to_be_bytes(), &s);
409 pending.push(s);
410 } else if in_bfrange {
411 pending.push(s);
412 if pending.len() == 3 {
413 apply_bfrange(&mut map, &pending[0], &pending[1], &pending[2]);
414 pending.clear();
415 }
416 }
417 }
418 }
419 _ => i += 1,
420 }
421 }
422 map
423}
424
425fn insert_tounicode(map: &mut HashMap<u16, String>, src: &[u8], dst: &[u8]) {
426 if src.len() == 2 {
427 let code = u16::from_be_bytes([src[0], src[1]]);
428 let s = utf16be_to_string(dst);
429 if !s.is_empty() {
430 map.insert(code, s);
431 }
432 }
433}
434
435fn apply_bfrange(map: &mut HashMap<u16, String>, lo: &[u8], hi: &[u8], dst: &[u8]) {
438 if lo.len() != 2 || hi.len() != 2 || dst.len() < 2 {
439 return;
440 }
441 let lo = u16::from_be_bytes([lo[0], lo[1]]);
442 let hi = u16::from_be_bytes([hi[0], hi[1]]);
443 let units: Vec<u16> = dst
444 .chunks_exact(2)
445 .map(|c| u16::from_be_bytes([c[0], c[1]]))
446 .collect();
447 let mut cur = *units.last().unwrap_or(&0);
448 for code in lo..=hi {
449 let mut s = String::new();
450 for (idx, u) in units.iter().enumerate() {
451 let v = if idx == units.len() - 1 { cur } else { *u };
452 s.push(char::from_u32(u32::from(v)).unwrap_or('\u{FFFD}'));
453 }
454 map.insert(code, s);
455 cur = cur.wrapping_add(1);
456 }
457}
458
459fn utf16be_to_string(bytes: &[u8]) -> String {
460 bytes
461 .chunks_exact(2)
462 .map(|c| {
463 let u = u16::from_be_bytes([c[0], c[1]]);
464 char::from_u32(u32::from(u)).unwrap_or('\u{FFFD}')
465 })
466 .collect()
467}
468
469fn push_text(bytes: &[u8], out: &mut String) {
470 if bytes.starts_with(&[0xfe, 0xff]) {
472 let utf16: Vec<u16> = bytes[2..]
473 .chunks_exact(2)
474 .map(|c| u16::from_be_bytes([c[0], c[1]]))
475 .collect();
476 out.push_str(&String::from_utf16_lossy(&utf16));
477 } else {
478 out.push_str(&String::from_utf8_lossy(bytes));
479 }
480}
481
482fn read_literal_string(data: &[u8], i: &mut usize) -> Option<Vec<u8>> {
485 debug_assert_eq!(data[*i], b'(');
486 *i += 1;
487 let mut out = Vec::new();
488 let mut depth = 1u32;
489 while *i < data.len() {
490 match data[*i] {
491 b'(' => {
492 depth += 1;
493 out.push(b'(');
494 *i += 1;
495 }
496 b')' => {
497 depth -= 1;
498 *i += 1;
499 if depth == 0 {
500 return Some(out);
501 }
502 out.push(b')');
503 }
504 b'\\' => {
505 *i += 1;
506 let Some(&esc) = data.get(*i) else {
507 return Some(out);
508 };
509 *i += 1;
510 match esc {
511 b'n' => out.push(b'\n'),
512 b'r' => out.push(b'\r'),
513 b't' => out.push(b'\t'),
514 b'b' => out.push(8),
515 b'f' => out.push(12),
516 b'(' => out.push(b'('),
517 b')' => out.push(b')'),
518 b'\\' => out.push(b'\\'),
519 b'0'..=b'7' => {
520 let mut v = esc - b'0';
521 for _ in 0..2 {
522 match data.get(*i) {
523 Some(e @ b'0'..=b'7') => {
524 v = v * 8 + (e - b'0');
525 *i += 1;
526 }
527 _ => break,
528 }
529 }
530 out.push(v);
531 }
532 b'\r' => {
533 if data.get(*i) == Some(&b'\n') {
534 *i += 1;
535 }
536 }
537 b'\n' => {}
538 other => out.push(other),
539 }
540 }
541 b'\r' => {
542 *i += 1;
543 if data.get(*i) == Some(&b'\n') {
544 *i += 1;
545 }
546 out.push(b'\n');
547 }
548 other => {
549 out.push(other);
550 *i += 1;
551 }
552 }
553 }
554 None
555}
556
557fn read_hex_string(data: &[u8], i: &mut usize) -> Option<Vec<u8>> {
559 debug_assert_eq!(data[*i], b'<');
560 *i += 1;
561 let mut out = Vec::new();
562 let mut hi: Option<u8> = None;
563 while *i < data.len() {
564 match data[*i] {
565 b'>' => {
566 *i += 1;
567 if let Some(h) = hi {
568 out.push(h << 4);
569 }
570 return Some(out);
571 }
572 b' ' | b'\t' | b'\r' | b'\n' => *i += 1,
573 b'0'..=b'9' | b'a'..=b'f' | b'A'..=b'F' => {
574 let v = match data[*i] {
575 b'0'..=b'9' => data[*i] - b'0',
576 b'a'..=b'f' => data[*i] - b'a' + 10,
577 _ => data[*i] - b'A' + 10,
578 };
579 *i += 1;
580 match hi {
581 None => hi = Some(v),
582 Some(h) => {
583 out.push((h << 4) | v);
584 hi = None;
585 }
586 }
587 }
588 _ => *i += 1,
589 }
590 }
591 None
592}
593
594#[cfg(test)]
595mod tests {
596 use super::*;
597
598 fn extract(text: &str, compressed: bool) -> String {
599 let mut doc = paperforge_pdf::PdfDocument::new();
601 doc.set_catalog(ObjectId::new(1, 0));
602 doc.add_object(
603 ObjectId::new(1, 0),
604 PdfObject::Dictionary({
605 let mut d = paperforge_pdf::PdfDictionary::new();
606 d.insert(
607 "Type",
608 PdfObject::Name(paperforge_pdf::PdfName::new("Catalog")),
609 );
610 d.insert("Pages", PdfObject::Reference(ObjectId::new(2, 0)));
611 d
612 }),
613 );
614 doc.add_object(
615 ObjectId::new(2, 0),
616 PdfObject::Dictionary({
617 let mut d = paperforge_pdf::PdfDictionary::new();
618 d.insert(
619 "Type",
620 PdfObject::Name(paperforge_pdf::PdfName::new("Pages")),
621 );
622 d.insert("Count", PdfObject::Integer(1));
623 let mut kids = paperforge_pdf::PdfArray::new();
624 kids.push(PdfObject::Reference(ObjectId::new(4, 0)));
625 d.insert("Kids", PdfObject::Array(kids));
626 d
627 }),
628 );
629 let mut escaped = Vec::new();
632 paperforge_pdf::escape_literal_string_into(&mut escaped, text.as_bytes());
633 let content = format!(
634 "BT /F1 12 Tf 50 700 Td ({}) Tj ET",
635 String::from_utf8_lossy(&escaped)
636 );
637 let data = if compressed {
638 paperforge_pdf::StreamEncoder::new()
639 .encode(content.as_bytes(), StreamFilter::Flate)
640 .unwrap()
641 } else {
642 content.as_bytes().to_vec()
643 };
644 let stream =
645 paperforge_pdf::PdfStream::with_dict(paperforge_pdf::PdfDictionary::new(), data);
646 doc.add_object(ObjectId::new(3, 0), PdfObject::Stream(stream));
647 doc.add_object(
648 ObjectId::new(4, 0),
649 PdfObject::Dictionary({
650 let mut d = paperforge_pdf::PdfDictionary::new();
651 d.insert(
652 "Type",
653 PdfObject::Name(paperforge_pdf::PdfName::new("Page")),
654 );
655 d.insert("Parent", PdfObject::Reference(ObjectId::new(2, 0)));
656 d.insert("Contents", PdfObject::Reference(ObjectId::new(3, 0)));
657 d
658 }),
659 );
660
661 let mut buf = std::io::Cursor::new(Vec::new());
662 paperforge_pdf::Serializer::new()
663 .serialize(&doc, &mut buf)
664 .unwrap();
665 Extractor::extract_text(&Extractor, buf.get_ref()).unwrap()
666 }
667
668 #[test]
669 fn extracts_simple_text() {
670 assert_eq!(extract("Hello, world!", false), "Hello, world!\n");
671 }
672
673 #[test]
674 fn extracts_escaped_parens_and_backslashes() {
675 assert_eq!(extract(r"a(b)c\d", false), "a(b)c\\d\n");
677 }
678
679 #[test]
680 fn extracts_from_compressed_streams() {
681 assert_eq!(extract("compressed", true), "compressed\n");
682 }
683
684 #[test]
685 fn metadata_does_not_leak_into_text() {
686 let mut doc = paperforge_pdf::PdfDocument::new();
688 doc.set_catalog(ObjectId::new(1, 0));
689 doc.add_object(
690 ObjectId::new(1, 0),
691 PdfObject::Dictionary({
692 let mut d = paperforge_pdf::PdfDictionary::new();
693 d.insert(
694 "Type",
695 PdfObject::Name(paperforge_pdf::PdfName::new("Catalog")),
696 );
697 d.insert("Pages", PdfObject::Reference(ObjectId::new(2, 0)));
698 d
699 }),
700 );
701 doc.add_object(
702 ObjectId::new(2, 0),
703 PdfObject::Dictionary({
704 let mut d = paperforge_pdf::PdfDictionary::new();
705 d.insert(
706 "Type",
707 PdfObject::Name(paperforge_pdf::PdfName::new("Pages")),
708 );
709 d.insert("Count", PdfObject::Integer(1));
710 let mut kids = paperforge_pdf::PdfArray::new();
711 kids.push(PdfObject::Reference(ObjectId::new(3, 0)));
712 d.insert("Kids", PdfObject::Array(kids));
713 d
714 }),
715 );
716 doc.add_object(
717 ObjectId::new(3, 0),
718 PdfObject::Dictionary({
719 let mut d = paperforge_pdf::PdfDictionary::new();
720 d.insert(
721 "Type",
722 PdfObject::Name(paperforge_pdf::PdfName::new("Page")),
723 );
724 d.insert("Parent", PdfObject::Reference(ObjectId::new(2, 0)));
725 d.insert("Contents", PdfObject::Reference(ObjectId::new(4, 0)));
726 d
727 }),
728 );
729 doc.add_object(
730 ObjectId::new(4, 0),
731 PdfObject::Stream(paperforge_pdf::PdfStream::with_dict(
732 paperforge_pdf::PdfDictionary::new(),
733 b"BT (page text) Tj ET".to_vec(),
734 )),
735 );
736 doc.set_info(ObjectId::new(5, 0));
737 doc.add_object(
738 ObjectId::new(5, 0),
739 PdfObject::Dictionary({
740 let mut d = paperforge_pdf::PdfDictionary::new();
741 d.insert(
742 "Producer",
743 PdfObject::String(paperforge_pdf::PdfString::from_literal("PaperForge")),
744 );
745 d
746 }),
747 );
748 let mut buf = std::io::Cursor::new(Vec::new());
749 paperforge_pdf::Serializer::new()
750 .serialize(&doc, &mut buf)
751 .unwrap();
752 let text = Extractor::extract_text(&Extractor, buf.get_ref()).unwrap();
753 assert_eq!(text, "page text\n");
754 }
755
756 fn embedded_font_fixture() -> Vec<u8> {
760 let mut doc = paperforge_pdf::PdfDocument::new();
761 doc.set_catalog(ObjectId::new(1, 0));
762 doc.add_object(
763 ObjectId::new(1, 0),
764 PdfObject::Dictionary({
765 let mut d = paperforge_pdf::PdfDictionary::new();
766 d.insert(
767 "Type",
768 PdfObject::Name(paperforge_pdf::PdfName::new("Catalog")),
769 );
770 d.insert("Pages", PdfObject::Reference(ObjectId::new(2, 0)));
771 d
772 }),
773 );
774 doc.add_object(
775 ObjectId::new(2, 0),
776 PdfObject::Dictionary({
777 let mut d = paperforge_pdf::PdfDictionary::new();
778 d.insert(
779 "Type",
780 PdfObject::Name(paperforge_pdf::PdfName::new("Pages")),
781 );
782 d.insert("Count", PdfObject::Integer(1));
783 let mut kids = paperforge_pdf::PdfArray::new();
784 kids.push(PdfObject::Reference(ObjectId::new(4, 0)));
785 d.insert("Kids", PdfObject::Array(kids));
786 d
787 }),
788 );
789
790 let cmap = concat!(
793 "/CIDInit /ProcSet findresource begin\n",
794 "12 dict begin\nbegincmap\n",
795 "/CMapType 2 def\n1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
796 "2 beginbfchar\n<0048> <0048>\n<0063> <0063>\nendbfchar\n", "1 beginbfrange\n<0041> <0043> <0041>\nendbfrange\n", "1 beginbfrange\n<0061> <0062> [<00E9> <00FC>]\nendbfrange\n", "endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n",
800 );
801 doc.add_object(
802 ObjectId::new(5, 0),
803 PdfObject::Stream(paperforge_pdf::PdfStream::with_dict(
804 paperforge_pdf::PdfDictionary::new(),
805 cmap.as_bytes().to_vec(),
806 )),
807 );
808
809 let mut font = paperforge_pdf::PdfDictionary::new();
811 font.insert(
812 "Type",
813 PdfObject::Name(paperforge_pdf::PdfName::new("Font")),
814 );
815 font.insert(
816 "Subtype",
817 PdfObject::Name(paperforge_pdf::PdfName::new("Type0")),
818 );
819 font.insert(
820 "Encoding",
821 PdfObject::Name(paperforge_pdf::PdfName::new("Identity-H")),
822 );
823 font.insert("ToUnicode", PdfObject::Reference(ObjectId::new(5, 0)));
824 doc.add_object(ObjectId::new(6, 0), PdfObject::Dictionary(font));
825
826 doc.add_object(
827 ObjectId::new(3, 0),
828 PdfObject::Stream(paperforge_pdf::PdfStream::with_dict(
829 paperforge_pdf::PdfDictionary::new(),
830 b"BT /F2 12 Tf 50 700 Td <0048006300410042004300610062> Tj ET".to_vec(),
831 )),
832 );
833 doc.add_object(
834 ObjectId::new(4, 0),
835 PdfObject::Dictionary({
836 let mut d = paperforge_pdf::PdfDictionary::new();
837 d.insert(
838 "Type",
839 PdfObject::Name(paperforge_pdf::PdfName::new("Page")),
840 );
841 d.insert("Parent", PdfObject::Reference(ObjectId::new(2, 0)));
842 d.insert("Contents", PdfObject::Reference(ObjectId::new(3, 0)));
843 let mut resources = paperforge_pdf::PdfDictionary::new();
844 let mut fonts = paperforge_pdf::PdfDictionary::new();
845 fonts.insert("F2", PdfObject::Reference(ObjectId::new(6, 0)));
846 resources.insert("Font", PdfObject::Dictionary(fonts));
847 d.insert("Resources", PdfObject::Dictionary(resources));
848 d
849 }),
850 );
851
852 let mut buf = std::io::Cursor::new(Vec::new());
853 paperforge_pdf::Serializer::new()
854 .serialize(&doc, &mut buf)
855 .unwrap();
856 buf.into_inner()
857 }
858
859 #[test]
862 fn extracts_embedded_font_text_via_tounicode() {
863 let data = embedded_font_fixture();
864 let text = Extractor::extract_text(&Extractor, &data).unwrap();
865 assert_eq!(text, "HcABCéü\n");
866 }
867}