1use crate::error::{ManipError, Result};
25use crate::text_edit::{
26 self, CommitPolicy, DocumentRevision, FontFallback, ReplaceOptions, SignaturePolicy,
27 TextEditError, TextQuery,
28};
29use crate::text_run::FontMap;
30use lopdf::{Document, Object};
31
32pub fn replace_text(
44 doc: &mut Document,
45 page_num: u32,
46 search: &str,
47 replacement: &str,
48 _fonts: &FontMap,
49) -> Result<usize> {
50 let total = doc.get_pages().len();
51 if page_num == 0 || page_num as usize > total {
52 return Err(ManipError::PageOutOfRange(page_num as usize, total));
53 }
54 legacy_replace(
55 doc,
56 TextQuery::exact(search).pages(page_num..=page_num),
57 replacement,
58 )
59}
60
61pub fn replace_text_all_pages(
66 doc: &mut Document,
67 search: &str,
68 replacement: &str,
69) -> Result<usize> {
70 legacy_replace(doc, TextQuery::exact(search), replacement)
71}
72
73fn legacy_replace(doc: &mut Document, query: TextQuery, replacement: &str) -> Result<usize> {
74 let revision = DocumentRevision::from_source_bytes(&[]);
77 let options = ReplaceOptions::default()
78 .font_fallback(FontFallback::InjectStandard)
79 .signature_policy(SignaturePolicy::AllowPostSignatureChange)
80 .commit_policy(CommitPolicy::BestEffort);
81 match text_edit::replace_text(doc, revision, query, replacement, options) {
82 Ok(report) => Ok(report.replacements_applied),
83 Err(e) => match e.error {
84 TextEditError::Document(m) => Err(m),
85 other => Err(ManipError::Other(format!(
86 "text replacement failed: {other}"
87 ))),
88 },
89 }
90}
91
92pub(crate) fn encode_text_for_font(
106 font_name: &str,
107 text: &str,
108 fonts: &FontMap,
109) -> Result<Vec<u8>> {
110 if fonts.is_cid_font(font_name) {
111 return encode_cid_text(font_name, text, fonts);
112 }
113
114 let reverse = fonts.build_reverse_map(font_name);
117 if !reverse.is_empty() {
118 let mut bytes = Vec::with_capacity(text.len());
119 for ch in text.chars() {
120 if let Some(&code) = reverse.get(&ch) {
121 if code <= 0xFF {
122 bytes.push(code as u8);
123 } else {
124 return Err(ManipError::Other(format!(
125 "character '{}' (U+{:04X}) maps to code {} which exceeds single-byte range in font '{}'",
126 ch, ch as u32, code, font_name
127 )));
128 }
129 } else {
130 return Err(ManipError::Other(format!(
131 "character '{}' (U+{:04X}) not available in font '{}' (subset or custom encoding)",
132 ch, ch as u32, font_name
133 )));
134 }
135 }
136 let decoded_back = fonts.decode_string(font_name, &bytes);
141 if decoded_back != text {
142 return Err(ManipError::Other(format!(
143 "encoding round-trip mismatch for font '{}': '{}' re-decodes as '{}'",
144 font_name, text, decoded_back
145 )));
146 }
147 return Ok(bytes);
148 }
149
150 if fonts.is_subset_font(font_name) {
163 return Err(ManipError::Other(format!(
164 "font '{}' is a subset with no known encoding — cannot safely encode replacement text",
165 font_name
166 )));
167 }
168 if fonts.is_symbolic_font(font_name) {
169 return Err(ManipError::Other(format!(
170 "font '{}' is symbolic with no known encoding — Latin-1 fallback unsafe",
171 font_name
172 )));
173 }
174
175 let mut bytes = Vec::with_capacity(text.len());
177 for ch in text.chars() {
178 let code = ch as u32;
179 if code <= 0xFF {
180 bytes.push(code as u8);
181 } else {
182 return Err(ManipError::Other(format!(
183 "character '{}' (U+{:04X}) cannot be encoded in font '{}'",
184 ch, code, font_name
185 )));
186 }
187 }
188 Ok(bytes)
189}
190
191fn encode_cid_text(font_name: &str, text: &str, fonts: &FontMap) -> Result<Vec<u8>> {
192 let reverse = fonts.build_reverse_map(font_name);
196 let mut bytes = Vec::with_capacity(text.len() * 2);
197
198 for ch in text.chars() {
199 if let Some(code) = reverse.get(&ch) {
200 bytes.push((*code >> 8) as u8);
201 bytes.push((*code & 0xFF) as u8);
202 } else {
203 return Err(ManipError::Other(format!(
204 "character '{}' (U+{:04X}) not in font '{}' CMap",
205 ch, ch as u32, font_name
206 )));
207 }
208 }
209
210 Ok(bytes)
211}
212
213pub(crate) fn encode_latin1(text: &str) -> Result<Vec<u8>> {
219 let mut bytes = Vec::with_capacity(text.len());
220 for ch in text.chars() {
221 let code = ch as u32;
222 if code <= 0xFF {
223 bytes.push(code as u8);
224 } else {
225 return Err(ManipError::Other(format!(
226 "character '{}' (U+{:04X}) cannot be encoded as Latin-1",
227 ch, code
228 )));
229 }
230 }
231 Ok(bytes)
232}
233
234pub(crate) fn inject_fallback_font(doc: &mut Document, page_num: u32) -> Option<String> {
240 const FALLBACK: &str = "F__Helv";
241
242 let pages = doc.get_pages();
243 let &page_id = pages.get(&page_num)?;
244
245 let mut helv_dict = lopdf::Dictionary::new();
247 helv_dict.set("Type", Object::Name(b"Font".to_vec()));
248 helv_dict.set("Subtype", Object::Name(b"Type1".to_vec()));
249 helv_dict.set("BaseFont", Object::Name(b"Helvetica".to_vec()));
250 helv_dict.set("Encoding", Object::Name(b"WinAnsiEncoding".to_vec()));
251 let helv_id = doc.add_object(Object::Dictionary(helv_dict));
252
253 let resources_entry = doc.get_object(page_id).ok().and_then(|obj| {
255 if let Object::Dictionary(ref d) = obj {
256 d.get(b"Resources").ok().cloned()
257 } else {
258 None
259 }
260 });
261
262 let inserted = match resources_entry {
269 Some(Object::Reference(res_id)) => {
270 let font_entry = doc.get_object(res_id).ok().and_then(|obj| {
271 if let Object::Dictionary(ref d) = obj {
272 d.get(b"Font").ok().cloned()
273 } else {
274 None
275 }
276 });
277 match font_entry {
278 Some(Object::Reference(fd_id)) => {
279 if let Ok(Object::Dictionary(ref mut fd)) = doc.get_object_mut(fd_id) {
280 fd.set(FALLBACK, Object::Reference(helv_id));
281 true
282 } else {
283 false
284 }
285 }
286 font_val => {
287 let mut new_font = match font_val {
288 Some(Object::Dictionary(fd)) => fd,
289 _ => lopdf::Dictionary::new(),
290 };
291 new_font.set(FALLBACK, Object::Reference(helv_id));
292 if let Ok(Object::Dictionary(ref mut rd)) = doc.get_object_mut(res_id) {
293 rd.set("Font", Object::Dictionary(new_font));
294 true
295 } else {
296 false
297 }
298 }
299 }
300 }
301 Some(Object::Dictionary(res_dict)) => {
302 let font_entry = res_dict.get(b"Font").ok().cloned();
303 match font_entry {
304 Some(Object::Reference(fd_id)) => {
305 if let Ok(Object::Dictionary(ref mut fd)) = doc.get_object_mut(fd_id) {
306 fd.set(FALLBACK, Object::Reference(helv_id));
307 true
308 } else {
309 false
310 }
311 }
312 font_val => {
313 let mut new_font = match font_val {
314 Some(Object::Dictionary(fd)) => fd,
315 _ => lopdf::Dictionary::new(),
316 };
317 new_font.set(FALLBACK, Object::Reference(helv_id));
318 let mut new_res = res_dict;
319 new_res.set("Font", Object::Dictionary(new_font));
320 if let Ok(Object::Dictionary(ref mut pd)) = doc.get_object_mut(page_id) {
321 pd.set("Resources", Object::Dictionary(new_res));
322 true
323 } else {
324 false
325 }
326 }
327 }
328 }
329 _ => {
330 let mut new_font = lopdf::Dictionary::new();
336 new_font.set(FALLBACK, Object::Reference(helv_id));
337 let mut new_res = lopdf::Dictionary::new();
338 new_res.set("Font", Object::Dictionary(new_font));
339 if let Ok(Object::Dictionary(ref mut pd)) = doc.get_object_mut(page_id) {
340 pd.set("Resources", Object::Dictionary(new_res));
341 true
342 } else {
343 false
344 }
345 }
346 };
347
348 if inserted {
349 Some(FALLBACK.to_string())
350 } else {
351 None
352 }
353}
354
355#[cfg(test)]
356mod tests {
357 use super::*;
358 use crate::content_editor::editor_for_page;
359 use crate::text_run::extract_text_runs;
360 use lopdf::content::{Content, Operation};
361 use lopdf::{dictionary, Document, Object, Stream};
362
363 fn make_doc_with_cid_font(content_bytes: Vec<u8>) -> Document {
368 let mut doc = Document::with_version("1.7");
369
370 let to_unicode_data = b"/CIDInit /ProcSet findresource begin\n\
372 12 dict begin\n\
373 begincmap\n\
374 /CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
375 /CMapName /Adobe-Identity-UCS def\n\
376 /CMapType 2 def\n\
377 1 begincodespacerange\n\
378 <0000> <FFFF>\n\
379 endcodespacerange\n\
380 1 beginbfrange\n\
381 <0020> <007E> <0020>\n\
382 endbfrange\n\
383 endcmap\n\
384 CMapName currentdict /CMap defineresource pop\n\
385 end\n\
386 end\n";
387 let to_unicode_stream = Stream::new(dictionary! {}, to_unicode_data.to_vec());
388 let to_unicode_id = doc.add_object(Object::Stream(to_unicode_stream));
389
390 let cid_font = dictionary! {
391 "Type" => Object::Name(b"Font".to_vec()),
392 "Subtype" => Object::Name(b"CIDFontType2".to_vec()),
393 "BaseFont" => Object::Name(b"TestCIDFont".to_vec()),
394 "CIDSystemInfo" => Object::Dictionary(dictionary! {
395 "Registry" => Object::String(b"Adobe".to_vec(), lopdf::StringFormat::Literal),
396 "Ordering" => Object::String(b"Identity".to_vec(), lopdf::StringFormat::Literal),
397 "Supplement" => Object::Integer(0),
398 }),
399 "DW" => Object::Integer(1000),
400 };
401 let cid_font_id = doc.add_object(Object::Dictionary(cid_font));
402
403 let type0_font = dictionary! {
404 "Type" => Object::Name(b"Font".to_vec()),
405 "Subtype" => Object::Name(b"Type0".to_vec()),
406 "BaseFont" => Object::Name(b"TestCIDFont".to_vec()),
407 "Encoding" => Object::Name(b"Identity-H".to_vec()),
408 "DescendantFonts" => Object::Array(vec![Object::Reference(cid_font_id)]),
409 "ToUnicode" => Object::Reference(to_unicode_id),
410 };
411 let type0_font_id = doc.add_object(Object::Dictionary(type0_font));
412
413 let font_resources = dictionary! {
414 "F1" => Object::Reference(type0_font_id),
415 };
416 let resources = dictionary! {
417 "Font" => Object::Dictionary(font_resources),
418 };
419
420 let content_stream = Stream::new(dictionary! {}, content_bytes);
421 let content_id = doc.add_object(Object::Stream(content_stream));
422
423 let page_dict = dictionary! {
424 "Type" => Object::Name(b"Page".to_vec()),
425 "MediaBox" => Object::Array(vec![
426 Object::Integer(0), Object::Integer(0),
427 Object::Integer(612), Object::Integer(792),
428 ]),
429 "Contents" => Object::Reference(content_id),
430 "Resources" => Object::Dictionary(resources),
431 };
432 let page_id = doc.add_object(Object::Dictionary(page_dict));
433
434 let pages_dict = dictionary! {
435 "Type" => Object::Name(b"Pages".to_vec()),
436 "Kids" => Object::Array(vec![Object::Reference(page_id)]),
437 "Count" => Object::Integer(1),
438 };
439 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
440
441 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
442 d.set("Parent", Object::Reference(pages_id));
443 }
444
445 let catalog = dictionary! {
446 "Type" => Object::Name(b"Catalog".to_vec()),
447 "Pages" => Object::Reference(pages_id),
448 };
449 let catalog_id = doc.add_object(Object::Dictionary(catalog));
450 doc.trailer.set("Root", Object::Reference(catalog_id));
451
452 doc
453 }
454
455 fn hello_cid_content() -> Vec<u8> {
457 let cid_bytes = vec![0x00u8, 0x48, 0x00, 0x65, 0x00, 0x6C, 0x00, 0x6C, 0x00, 0x6F];
459 Content {
460 operations: vec![
461 Operation::new("BT", vec![]),
462 Operation::new("Tf", vec![Object::Name(b"F1".to_vec()), Object::Real(12.0)]),
463 Operation::new("Td", vec![Object::Real(100.0), Object::Real(700.0)]),
464 Operation::new(
465 "Tj",
466 vec![Object::String(cid_bytes, lopdf::StringFormat::Hexadecimal)],
467 ),
468 Operation::new("ET", vec![]),
469 ],
470 }
471 .encode()
472 .unwrap()
473 }
474
475 fn make_doc_with_text(content: &[u8]) -> Document {
476 let mut doc = Document::with_version("1.7");
477
478 let font = dictionary! {
479 "Type" => "Font",
480 "Subtype" => "Type1",
481 "BaseFont" => "Helvetica",
482 };
483 let font_id = doc.add_object(Object::Dictionary(font));
484
485 let font_resources = dictionary! {
486 "F1" => Object::Reference(font_id),
487 };
488 let resources = dictionary! {
489 "Font" => Object::Dictionary(font_resources),
490 };
491
492 let content_stream = Stream::new(dictionary! {}, content.to_vec());
493 let content_id = doc.add_object(Object::Stream(content_stream));
494
495 let page_dict = dictionary! {
496 "Type" => "Page",
497 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
498 "Contents" => Object::Reference(content_id),
499 "Resources" => Object::Dictionary(resources),
500 };
501 let page_id = doc.add_object(Object::Dictionary(page_dict));
502
503 let pages_dict = dictionary! {
504 "Type" => "Pages",
505 "Kids" => vec![Object::Reference(page_id)],
506 "Count" => 1_i64,
507 };
508 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
509
510 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
511 d.set("Parent", Object::Reference(pages_id));
512 }
513
514 let catalog = dictionary! {
515 "Type" => "Catalog",
516 "Pages" => Object::Reference(pages_id),
517 };
518 let catalog_id = doc.add_object(Object::Dictionary(catalog));
519 doc.trailer.set("Root", Object::Reference(catalog_id));
520
521 doc
522 }
523
524 #[test]
525 fn replace_simple_same_length() {
526 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET");
527 let fonts = FontMap::from_page(&doc, 1).unwrap();
528 let count = replace_text(&mut doc, 1, "Hello", "Hallo", &fonts).unwrap();
529 assert_eq!(count, 1);
530
531 let editor = editor_for_page(&doc, 1).unwrap();
533 let runs = extract_text_runs(&editor, &fonts);
534 assert_eq!(runs.len(), 1);
535 assert_eq!(runs[0].text, "Hallo World");
536 }
537
538 #[test]
539 fn replace_different_length() {
540 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET");
541 let fonts = FontMap::from_page(&doc, 1).unwrap();
542 let count = replace_text(&mut doc, 1, "World", "Earth!", &fonts).unwrap();
543 assert_eq!(count, 1);
544
545 let editor = editor_for_page(&doc, 1).unwrap();
546 let runs = extract_text_runs(&editor, &fonts);
547 assert_eq!(runs.len(), 1);
548 assert_eq!(runs[0].text, "Hello Earth!");
549 }
550
551 #[test]
552 fn replace_no_match() {
553 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Hello) Tj ET");
554 let fonts = FontMap::from_page(&doc, 1).unwrap();
555 let count = replace_text(&mut doc, 1, "Missing", "Replacement", &fonts).unwrap();
556 assert_eq!(count, 0);
557 }
558
559 #[test]
560 fn replace_in_tj_array() {
561 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td [(Hel) -100 (lo)] TJ ET");
562 let fonts = FontMap::from_page(&doc, 1).unwrap();
563 let count = replace_text(&mut doc, 1, "Hello", "Hallo", &fonts).unwrap();
564 assert_eq!(count, 1);
565
566 let editor = editor_for_page(&doc, 1).unwrap();
567 let runs = extract_text_runs(&editor, &fonts);
568 assert_eq!(runs[0].text, "Hallo");
569 }
570
571 #[test]
572 fn replace_error_on_unencodable_char() {
573 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Hello) Tj ET");
574 let fonts = FontMap::from_page(&doc, 1).unwrap();
575 let result = replace_text(&mut doc, 1, "Hello", "\u{4e16}\u{754c}", &fonts);
578 assert_eq!(result.unwrap(), 0);
579 }
580
581 #[test]
582 fn replace_all_pages() {
583 let mut doc = Document::with_version("1.7");
584
585 let font = dictionary! {
586 "Type" => "Font",
587 "Subtype" => "Type1",
588 "BaseFont" => "Helvetica",
589 };
590 let font_id = doc.add_object(Object::Dictionary(font));
591
592 let font_resources = dictionary! {
593 "F1" => Object::Reference(font_id),
594 };
595 let resources = dictionary! {
596 "Font" => Object::Dictionary(font_resources),
597 };
598
599 let mut page_ids = Vec::new();
601 for _ in 0..2 {
602 let content = b"BT /F1 12 Tf (Hello) Tj ET";
603 let content_stream = Stream::new(dictionary! {}, content.to_vec());
604 let content_id = doc.add_object(Object::Stream(content_stream));
605
606 let page_dict = dictionary! {
607 "Type" => "Page",
608 "MediaBox" => vec![0.into(), 0.into(), 612.into(), 792.into()],
609 "Contents" => Object::Reference(content_id),
610 "Resources" => Object::Dictionary(resources.clone()),
611 };
612 let page_id = doc.add_object(Object::Dictionary(page_dict));
613 page_ids.push(page_id);
614 }
615
616 let pages_dict = dictionary! {
617 "Type" => "Pages",
618 "Kids" => page_ids.iter().map(|id| Object::Reference(*id)).collect::<Vec<_>>(),
619 "Count" => page_ids.len() as i64,
620 };
621 let pages_id = doc.add_object(Object::Dictionary(pages_dict));
622
623 for &page_id in &page_ids {
624 if let Ok(Object::Dictionary(ref mut d)) = doc.get_object_mut(page_id) {
625 d.set("Parent", Object::Reference(pages_id));
626 }
627 }
628
629 let catalog = dictionary! {
630 "Type" => "Catalog",
631 "Pages" => Object::Reference(pages_id),
632 };
633 let catalog_id = doc.add_object(Object::Dictionary(catalog));
634 doc.trailer.set("Root", Object::Reference(catalog_id));
635
636 let count = replace_text_all_pages(&mut doc, "Hello", "Hallo").unwrap();
637 assert_eq!(count, 2);
638 }
639
640 #[test]
641 fn replace_cross_run_split_tj() {
642 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Jan) Tj (u) Tj (ary) Tj ET");
644 let fonts = FontMap::from_page(&doc, 1).unwrap();
645 let count = replace_text(&mut doc, 1, "January", "Februar", &fonts).unwrap();
646 assert_eq!(count, 1);
647
648 let editor = editor_for_page(&doc, 1).unwrap();
650 let runs = extract_text_runs(&editor, &fonts);
651 let combined: String = runs.iter().map(|r| r.text.as_str()).collect();
652 assert!(
653 combined.contains("Februar"),
654 "expected 'Februar' in '{combined}'"
655 );
656 }
657
658 #[test]
659 fn replace_cross_run_with_positioning() {
660 let mut doc =
662 make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Hel) Tj 0.5 0 Td (lo World) Tj ET");
663 let fonts = FontMap::from_page(&doc, 1).unwrap();
664 let count = replace_text(&mut doc, 1, "Hello", "Hallo", &fonts).unwrap();
665 assert_eq!(count, 1);
666
667 let editor = editor_for_page(&doc, 1).unwrap();
668 let runs = extract_text_runs(&editor, &fonts);
669 let combined: String = runs.iter().map(|r| r.text.as_str()).collect();
670 assert!(
671 combined.contains("Hallo"),
672 "expected 'Hallo' in '{combined}'"
673 );
674 }
675
676 #[test]
677 fn replace_cross_run_single_run_takes_priority() {
678 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 100 700 Td (Hello World) Tj ET");
680 let fonts = FontMap::from_page(&doc, 1).unwrap();
681 let count = replace_text(&mut doc, 1, "Hello", "Hallo", &fonts).unwrap();
682 assert_eq!(count, 1);
683
684 let editor = editor_for_page(&doc, 1).unwrap();
685 let runs = extract_text_runs(&editor, &fonts);
686 assert_eq!(runs[0].text, "Hallo World");
687 }
688
689 #[test]
690 fn replace_cross_run_multiline_preserves_other_lines() {
691 let mut doc = make_doc_with_text(
698 b"BT /F1 12 Tf 72 700 Td (First line here.) Tj 0 -14 Td (Some words to ) Tj (edit now.) Tj 0 -14 Td (Third line stays.) Tj ET",
699 );
700 let fonts = FontMap::from_page(&doc, 1).unwrap();
701 let count = replace_text(
702 &mut doc,
703 1,
704 "Some words to edit now.",
705 "Some words to edit.",
706 &fonts,
707 )
708 .unwrap();
709 assert_eq!(count, 1);
710
711 let editor = editor_for_page(&doc, 1).unwrap();
712 let runs = extract_text_runs(&editor, &fonts);
713
714 assert_eq!(
716 runs.first().map(|r| r.text.as_str()),
717 Some("First line here."),
718 "first line must keep only its own text"
719 );
720 assert_eq!(
721 runs.last().map(|r| r.text.as_str()),
722 Some("Third line stays."),
723 "third line must not be emptied"
724 );
725
726 let combined: String = runs.iter().map(|r| r.text.as_str()).collect();
728 assert_eq!(
729 combined,
730 "First line here.Some words to edit.Third line stays."
731 );
732
733 let first_y = runs.first().unwrap().y;
735 let last_y = runs.last().unwrap().y;
736 assert!(
737 (first_y - last_y).abs() > 20.0,
738 "lines collapsed onto one baseline: first_y={first_y} last_y={last_y}"
739 );
740 }
741
742 #[test]
743 fn replace_cross_run_keeps_prefix_and_suffix_in_their_runs() {
744 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 72 700 Td (AAA xx) Tj (yy BBB) Tj ET");
747 let fonts = FontMap::from_page(&doc, 1).unwrap();
748 let count = replace_text(&mut doc, 1, "xxyy", "Q", &fonts).unwrap();
749 assert_eq!(count, 1);
750
751 let editor = editor_for_page(&doc, 1).unwrap();
752 let runs = extract_text_runs(&editor, &fonts);
753 assert_eq!(runs.len(), 2);
754 assert_eq!(runs[0].text, "AAA Q");
755 assert_eq!(runs[1].text, " BBB");
756 }
757
758 #[test]
759 fn replace_cross_run_multiple_occurrences_in_group() {
760 let mut doc = make_doc_with_text(b"BT /F1 12 Tf 72 700 Td (ab) Tj (cab) Tj (c) Tj ET");
764 let fonts = FontMap::from_page(&doc, 1).unwrap();
765 let count = replace_text(&mut doc, 1, "abc", "X", &fonts).unwrap();
766 assert_eq!(count, 2);
767
768 let editor = editor_for_page(&doc, 1).unwrap();
769 let runs = extract_text_runs(&editor, &fonts);
770 let combined: String = runs.iter().map(|r| r.text.as_str()).collect();
771 assert_eq!(combined, "XX");
772 }
773
774 #[test]
777 fn replace_cid_font_chars_in_cmap() {
778 let mut doc = make_doc_with_cid_font(hello_cid_content());
782 let fonts = FontMap::from_page(&doc, 1).unwrap();
783
784 assert!(fonts.is_cid_font("F1"), "F1 must be recognized as CID font");
785
786 let count = replace_text(&mut doc, 1, "Hello", "Hallo", &fonts).unwrap();
787 assert_eq!(count, 1);
788
789 let fonts_after = FontMap::from_page(&doc, 1).unwrap();
791 let editor = editor_for_page(&doc, 1).unwrap();
792 let runs = extract_text_runs(&editor, &fonts_after);
793 let combined: String = runs.iter().map(|r| r.text.as_str()).collect();
794 assert!(
795 combined.contains("Hallo"),
796 "expected 'Hallo' in '{combined}'"
797 );
798 assert!(
799 !combined.contains("Hello"),
800 "old text 'Hello' should be gone"
801 );
802 }
803
804 #[test]
805 fn replace_cid_font_no_match() {
806 let mut doc = make_doc_with_cid_font(hello_cid_content());
807 let fonts = FontMap::from_page(&doc, 1).unwrap();
808 let count = replace_text(&mut doc, 1, "World", "Earth", &fonts).unwrap();
809 assert_eq!(count, 0);
810 }
811
812 #[test]
813 fn replace_cid_font_fallback_chars_not_in_cmap() {
814 let mut doc = make_doc_with_cid_font(hello_cid_content());
819 let fonts = FontMap::from_page(&doc, 1).unwrap();
820 let count = replace_text(&mut doc, 1, "Hello", "Hal\u{3053}", &fonts).unwrap();
822 assert_eq!(count, 0);
825 }
826
827 #[test]
828 fn replace_cid_font_latin_fallback() {
829 let mut doc = make_doc_with_cid_font(hello_cid_content());
834 let fonts = FontMap::from_page(&doc, 1).unwrap();
835 let count = replace_text(&mut doc, 1, "Hello", "Hêllo", &fonts).unwrap();
838 assert_eq!(count, 1);
839 }
840}