1pub mod cmap;
2mod encoding;
3pub mod extraction;
4mod extraction_cmap;
5mod flow;
6mod font;
7pub mod font_manager;
8pub mod fonts;
9mod header_footer;
10pub mod invoice;
11mod layout;
12mod list;
13pub mod metrics;
14pub mod ocr;
15pub mod plaintext;
16pub mod structured;
17pub mod table;
18pub mod table_detection;
19pub mod validation;
20
21#[cfg(test)]
22mod cmap_tests;
23
24#[cfg(feature = "ocr-tesseract")]
25pub mod tesseract_provider;
26
27pub use encoding::TextEncoding;
28pub use extraction::{
29 sanitize_extracted_text, ExtractedText, ExtractionOptions, TextExtractor, TextFragment,
30};
31pub use flow::{TextAlign, TextFlowContext};
32pub use font::{Font, FontEncoding, FontFamily, FontWithEncoding};
33pub use font_manager::{CustomFont, FontDescriptor, FontFlags, FontManager, FontMetrics, FontType};
34pub use header_footer::{HeaderFooter, HeaderFooterOptions, HeaderFooterPosition};
35pub use layout::{ColumnContent, ColumnLayout, ColumnOptions, TextFormat};
36pub use list::{
37 BulletStyle, ListElement, ListItem, ListOptions, ListStyle as ListStyleEnum, OrderedList,
38 OrderedListStyle, UnorderedList,
39};
40pub use metrics::{measure_char, measure_text, split_into_words};
41pub use ocr::{
42 CharacterConfidence, CorrectionCandidate, CorrectionReason, CorrectionSuggestion,
43 CorrectionType, FragmentType, ImagePreprocessing, MockOcrProvider, OcrEngine, OcrError,
44 OcrOptions, OcrPostProcessor, OcrProcessingResult, OcrProvider, OcrRegion, OcrResult,
45 OcrTextFragment, WordConfidence,
46};
47pub use plaintext::{LineBreakMode, PlainTextConfig, PlainTextExtractor, PlainTextResult};
48pub use table::{HeaderStyle, Table, TableCell, TableOptions};
49pub use validation::{MatchType, TextMatch, TextValidationResult, TextValidator};
50
51#[cfg(feature = "ocr-tesseract")]
52pub use tesseract_provider::{RustyTesseractConfig, RustyTesseractProvider};
53
54use crate::error::Result;
55use crate::Color;
56use std::collections::HashSet;
57use std::fmt::Write;
58
59#[derive(Clone, Copy, Debug, PartialEq, Eq)]
61pub enum TextRenderingMode {
62 Fill = 0,
64 Stroke = 1,
66 FillStroke = 2,
68 Invisible = 3,
70 FillClip = 4,
72 StrokeClip = 5,
74 FillStrokeClip = 6,
76 Clip = 7,
78}
79
80#[derive(Clone)]
81pub struct TextContext {
82 operations: String,
83 current_font: Font,
84 font_size: f64,
85 text_matrix: [f64; 6],
86 pending_position: Option<(f64, f64)>,
88 character_spacing: Option<f64>,
90 word_spacing: Option<f64>,
91 horizontal_scaling: Option<f64>,
92 leading: Option<f64>,
93 text_rise: Option<f64>,
94 rendering_mode: Option<TextRenderingMode>,
95 fill_color: Option<Color>,
97 stroke_color: Option<Color>,
98 used_characters: HashSet<char>,
100}
101
102impl Default for TextContext {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108impl TextContext {
109 pub fn new() -> Self {
110 Self {
111 operations: String::new(),
112 current_font: Font::Helvetica,
113 font_size: 12.0,
114 text_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
115 pending_position: None,
116 character_spacing: None,
117 word_spacing: None,
118 horizontal_scaling: None,
119 leading: None,
120 text_rise: None,
121 rendering_mode: None,
122 fill_color: None,
123 stroke_color: None,
124 used_characters: HashSet::new(),
125 }
126 }
127
128 pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
133 if self.used_characters.is_empty() {
134 None
135 } else {
136 Some(self.used_characters.clone())
137 }
138 }
139
140 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
141 self.current_font = font;
142 self.font_size = size;
143 self
144 }
145
146 #[allow(dead_code)]
148 pub(crate) fn current_font(&self) -> &Font {
149 &self.current_font
150 }
151
152 pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
153 self.text_matrix[4] = x;
155 self.text_matrix[5] = y;
156 self.pending_position = Some((x, y));
157 self
158 }
159
160 pub fn write(&mut self, text: &str) -> Result<&mut Self> {
161 self.operations.push_str("BT\n");
163
164 writeln!(
166 &mut self.operations,
167 "/{} {} Tf",
168 self.current_font.pdf_name(),
169 self.font_size
170 )
171 .expect("Writing to String should never fail");
172
173 self.apply_text_state_parameters();
175
176 let (x, y) = if let Some((px, py)) = self.pending_position.take() {
178 (px, py)
180 } else {
181 (self.text_matrix[4], self.text_matrix[5])
183 };
184
185 writeln!(&mut self.operations, "{:.2} {:.2} Td", x, y)
186 .expect("Writing to String should never fail");
187
188 match &self.current_font {
190 Font::Custom(_) => {
191 let utf16_units: Vec<u16> = text.encode_utf16().collect();
193 let mut utf16be_bytes = Vec::new();
194
195 for unit in utf16_units {
196 utf16be_bytes.push((unit >> 8) as u8); utf16be_bytes.push((unit & 0xFF) as u8); }
199
200 self.operations.push('<');
202 for &byte in &utf16be_bytes {
203 write!(&mut self.operations, "{:02X}", byte)
204 .expect("Writing to String should never fail");
205 }
206 self.operations.push_str("> Tj\n");
207 }
208 _ => {
209 let encoding = TextEncoding::WinAnsiEncoding;
211 let encoded_bytes = encoding.encode(text);
212
213 self.operations.push('(');
215 for &byte in &encoded_bytes {
216 match byte {
217 b'(' => self.operations.push_str("\\("),
218 b')' => self.operations.push_str("\\)"),
219 b'\\' => self.operations.push_str("\\\\"),
220 b'\n' => self.operations.push_str("\\n"),
221 b'\r' => self.operations.push_str("\\r"),
222 b'\t' => self.operations.push_str("\\t"),
223 0x20..=0x7E => self.operations.push(byte as char),
225 _ => write!(&mut self.operations, "\\{byte:03o}")
227 .expect("Writing to String should never fail"),
228 }
229 }
230 self.operations.push_str(") Tj\n");
231 }
232 }
233
234 self.used_characters.extend(text.chars());
236
237 self.operations.push_str("ET\n");
239
240 Ok(self)
241 }
242
243 pub fn write_line(&mut self, text: &str) -> Result<&mut Self> {
244 self.write(text)?;
245 self.text_matrix[5] -= self.font_size * 1.2; Ok(self)
247 }
248
249 pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
250 self.character_spacing = Some(spacing);
251 self
252 }
253
254 pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
255 self.word_spacing = Some(spacing);
256 self
257 }
258
259 pub fn set_horizontal_scaling(&mut self, scale: f64) -> &mut Self {
260 self.horizontal_scaling = Some(scale);
261 self
262 }
263
264 pub fn set_leading(&mut self, leading: f64) -> &mut Self {
265 self.leading = Some(leading);
266 self
267 }
268
269 pub fn set_text_rise(&mut self, rise: f64) -> &mut Self {
270 self.text_rise = Some(rise);
271 self
272 }
273
274 pub fn set_rendering_mode(&mut self, mode: TextRenderingMode) -> &mut Self {
276 self.rendering_mode = Some(mode);
277 self
278 }
279
280 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
282 self.fill_color = Some(color);
283 self
284 }
285
286 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
288 self.stroke_color = Some(color);
289 self
290 }
291
292 fn apply_text_state_parameters(&mut self) {
294 if let Some(spacing) = self.character_spacing {
296 writeln!(&mut self.operations, "{spacing:.2} Tc")
297 .expect("Writing to String should never fail");
298 }
299
300 if let Some(spacing) = self.word_spacing {
302 writeln!(&mut self.operations, "{spacing:.2} Tw")
303 .expect("Writing to String should never fail");
304 }
305
306 if let Some(scale) = self.horizontal_scaling {
308 writeln!(&mut self.operations, "{:.2} Tz", scale * 100.0)
309 .expect("Writing to String should never fail");
310 }
311
312 if let Some(leading) = self.leading {
314 writeln!(&mut self.operations, "{leading:.2} TL")
315 .expect("Writing to String should never fail");
316 }
317
318 if let Some(rise) = self.text_rise {
320 writeln!(&mut self.operations, "{rise:.2} Ts")
321 .expect("Writing to String should never fail");
322 }
323
324 if let Some(mode) = self.rendering_mode {
326 writeln!(&mut self.operations, "{} Tr", mode as u8)
327 .expect("Writing to String should never fail");
328 }
329
330 if let Some(color) = self.fill_color {
332 match color {
333 Color::Rgb(r, g, b) => {
334 writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} rg")
335 .expect("Writing to String should never fail");
336 }
337 Color::Gray(gray) => {
338 writeln!(&mut self.operations, "{gray:.3} g")
339 .expect("Writing to String should never fail");
340 }
341 Color::Cmyk(c, m, y, k) => {
342 writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} k")
343 .expect("Writing to String should never fail");
344 }
345 }
346 }
347
348 if let Some(color) = self.stroke_color {
350 match color {
351 Color::Rgb(r, g, b) => {
352 writeln!(&mut self.operations, "{r:.3} {g:.3} {b:.3} RG")
353 .expect("Writing to String should never fail");
354 }
355 Color::Gray(gray) => {
356 writeln!(&mut self.operations, "{gray:.3} G")
357 .expect("Writing to String should never fail");
358 }
359 Color::Cmyk(c, m, y, k) => {
360 writeln!(&mut self.operations, "{c:.3} {m:.3} {y:.3} {k:.3} K")
361 .expect("Writing to String should never fail");
362 }
363 }
364 }
365 }
366
367 pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
368 Ok(self.operations.as_bytes().to_vec())
369 }
370
371 pub(crate) fn append_raw_operation(&mut self, operation: &str) {
376 self.operations.push_str(operation);
377 }
378
379 pub fn font_size(&self) -> f64 {
381 self.font_size
382 }
383
384 pub fn text_matrix(&self) -> [f64; 6] {
386 self.text_matrix
387 }
388
389 pub fn position(&self) -> (f64, f64) {
391 (self.text_matrix[4], self.text_matrix[5])
392 }
393
394 pub fn clear(&mut self) {
396 self.operations.clear();
397 self.character_spacing = None;
398 self.word_spacing = None;
399 self.horizontal_scaling = None;
400 self.leading = None;
401 self.text_rise = None;
402 self.rendering_mode = None;
403 self.fill_color = None;
404 self.stroke_color = None;
405 }
406
407 pub fn operations(&self) -> &str {
409 &self.operations
410 }
411
412 #[cfg(test)]
414 pub fn generate_text_state_operations(&self) -> String {
415 let mut ops = String::new();
416
417 if let Some(spacing) = self.character_spacing {
419 writeln!(&mut ops, "{spacing:.2} Tc").unwrap();
420 }
421
422 if let Some(spacing) = self.word_spacing {
424 writeln!(&mut ops, "{spacing:.2} Tw").unwrap();
425 }
426
427 if let Some(scale) = self.horizontal_scaling {
429 writeln!(&mut ops, "{:.2} Tz", scale * 100.0).unwrap();
430 }
431
432 if let Some(leading) = self.leading {
434 writeln!(&mut ops, "{leading:.2} TL").unwrap();
435 }
436
437 if let Some(rise) = self.text_rise {
439 writeln!(&mut ops, "{rise:.2} Ts").unwrap();
440 }
441
442 if let Some(mode) = self.rendering_mode {
444 writeln!(&mut ops, "{} Tr", mode as u8).unwrap();
445 }
446
447 ops
448 }
449}
450
451#[cfg(test)]
452mod tests {
453 use super::*;
454
455 #[test]
456 fn test_text_context_new() {
457 let context = TextContext::new();
458 assert_eq!(context.current_font, Font::Helvetica);
459 assert_eq!(context.font_size, 12.0);
460 assert_eq!(context.text_matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
461 assert!(context.operations.is_empty());
462 }
463
464 #[test]
465 fn test_text_context_default() {
466 let context = TextContext::default();
467 assert_eq!(context.current_font, Font::Helvetica);
468 assert_eq!(context.font_size, 12.0);
469 }
470
471 #[test]
472 fn test_set_font() {
473 let mut context = TextContext::new();
474 context.set_font(Font::TimesBold, 14.0);
475 assert_eq!(context.current_font, Font::TimesBold);
476 assert_eq!(context.font_size, 14.0);
477 }
478
479 #[test]
480 fn test_position() {
481 let mut context = TextContext::new();
482 context.at(100.0, 200.0);
483 let (x, y) = context.position();
484 assert_eq!(x, 100.0);
485 assert_eq!(y, 200.0);
486 assert_eq!(context.text_matrix[4], 100.0);
487 assert_eq!(context.text_matrix[5], 200.0);
488 }
489
490 #[test]
491 fn test_write_simple_text() {
492 let mut context = TextContext::new();
493 context.write("Hello").unwrap();
494
495 let ops = context.operations();
496 assert!(ops.contains("BT\n"));
497 assert!(ops.contains("ET\n"));
498 assert!(ops.contains("/Helvetica 12 Tf"));
499 assert!(ops.contains("(Hello) Tj"));
500 }
501
502 #[test]
503 fn test_write_text_with_escaping() {
504 let mut context = TextContext::new();
505 context.write("(Hello)").unwrap();
506
507 let ops = context.operations();
508 assert!(ops.contains("(\\(Hello\\)) Tj"));
509 }
510
511 #[test]
512 fn test_write_line() {
513 let mut context = TextContext::new();
514 let initial_y = context.text_matrix[5];
515 context.write_line("Line 1").unwrap();
516
517 let new_y = context.text_matrix[5];
519 assert!(new_y < initial_y);
520 assert_eq!(new_y, initial_y - 12.0 * 1.2); }
522
523 #[test]
524 fn test_character_spacing() {
525 let mut context = TextContext::new();
526 context.set_character_spacing(2.5);
527
528 let ops = context.generate_text_state_operations();
529 assert!(ops.contains("2.50 Tc"));
530 }
531
532 #[test]
533 fn test_word_spacing() {
534 let mut context = TextContext::new();
535 context.set_word_spacing(1.5);
536
537 let ops = context.generate_text_state_operations();
538 assert!(ops.contains("1.50 Tw"));
539 }
540
541 #[test]
542 fn test_horizontal_scaling() {
543 let mut context = TextContext::new();
544 context.set_horizontal_scaling(1.25);
545
546 let ops = context.generate_text_state_operations();
547 assert!(ops.contains("125.00 Tz")); }
549
550 #[test]
551 fn test_leading() {
552 let mut context = TextContext::new();
553 context.set_leading(15.0);
554
555 let ops = context.generate_text_state_operations();
556 assert!(ops.contains("15.00 TL"));
557 }
558
559 #[test]
560 fn test_text_rise() {
561 let mut context = TextContext::new();
562 context.set_text_rise(3.0);
563
564 let ops = context.generate_text_state_operations();
565 assert!(ops.contains("3.00 Ts"));
566 }
567
568 #[test]
569 fn test_clear() {
570 let mut context = TextContext::new();
571 context.write("Hello").unwrap();
572 assert!(!context.operations().is_empty());
573
574 context.clear();
575 assert!(context.operations().is_empty());
576 }
577
578 #[test]
579 fn test_generate_operations() {
580 let mut context = TextContext::new();
581 context.write("Test").unwrap();
582
583 let ops_bytes = context.generate_operations().unwrap();
584 let ops_string = String::from_utf8(ops_bytes).unwrap();
585 assert_eq!(ops_string, context.operations());
586 }
587
588 #[test]
589 fn test_method_chaining() {
590 let mut context = TextContext::new();
591 context
592 .set_font(Font::Courier, 10.0)
593 .at(50.0, 100.0)
594 .set_character_spacing(1.0)
595 .set_word_spacing(2.0);
596
597 assert_eq!(context.current_font(), &Font::Courier);
598 assert_eq!(context.font_size(), 10.0);
599 let (x, y) = context.position();
600 assert_eq!(x, 50.0);
601 assert_eq!(y, 100.0);
602 }
603
604 #[test]
605 fn test_text_matrix_access() {
606 let mut context = TextContext::new();
607 context.at(25.0, 75.0);
608
609 let matrix = context.text_matrix();
610 assert_eq!(matrix, [1.0, 0.0, 0.0, 1.0, 25.0, 75.0]);
611 }
612
613 #[test]
614 fn test_special_characters_encoding() {
615 let mut context = TextContext::new();
616 context.write("Test\nLine\tTab").unwrap();
617
618 let ops = context.operations();
619 assert!(ops.contains("\\n"));
620 assert!(ops.contains("\\t"));
621 }
622
623 #[test]
624 fn test_rendering_mode_fill() {
625 let mut context = TextContext::new();
626 context.set_rendering_mode(TextRenderingMode::Fill);
627
628 let ops = context.generate_text_state_operations();
629 assert!(ops.contains("0 Tr"));
630 }
631
632 #[test]
633 fn test_rendering_mode_stroke() {
634 let mut context = TextContext::new();
635 context.set_rendering_mode(TextRenderingMode::Stroke);
636
637 let ops = context.generate_text_state_operations();
638 assert!(ops.contains("1 Tr"));
639 }
640
641 #[test]
642 fn test_rendering_mode_fill_stroke() {
643 let mut context = TextContext::new();
644 context.set_rendering_mode(TextRenderingMode::FillStroke);
645
646 let ops = context.generate_text_state_operations();
647 assert!(ops.contains("2 Tr"));
648 }
649
650 #[test]
651 fn test_rendering_mode_invisible() {
652 let mut context = TextContext::new();
653 context.set_rendering_mode(TextRenderingMode::Invisible);
654
655 let ops = context.generate_text_state_operations();
656 assert!(ops.contains("3 Tr"));
657 }
658
659 #[test]
660 fn test_rendering_mode_fill_clip() {
661 let mut context = TextContext::new();
662 context.set_rendering_mode(TextRenderingMode::FillClip);
663
664 let ops = context.generate_text_state_operations();
665 assert!(ops.contains("4 Tr"));
666 }
667
668 #[test]
669 fn test_rendering_mode_stroke_clip() {
670 let mut context = TextContext::new();
671 context.set_rendering_mode(TextRenderingMode::StrokeClip);
672
673 let ops = context.generate_text_state_operations();
674 assert!(ops.contains("5 Tr"));
675 }
676
677 #[test]
678 fn test_rendering_mode_fill_stroke_clip() {
679 let mut context = TextContext::new();
680 context.set_rendering_mode(TextRenderingMode::FillStrokeClip);
681
682 let ops = context.generate_text_state_operations();
683 assert!(ops.contains("6 Tr"));
684 }
685
686 #[test]
687 fn test_rendering_mode_clip() {
688 let mut context = TextContext::new();
689 context.set_rendering_mode(TextRenderingMode::Clip);
690
691 let ops = context.generate_text_state_operations();
692 assert!(ops.contains("7 Tr"));
693 }
694
695 #[test]
696 fn test_text_state_parameters_chaining() {
697 let mut context = TextContext::new();
698 context
699 .set_character_spacing(1.5)
700 .set_word_spacing(2.0)
701 .set_horizontal_scaling(1.1)
702 .set_leading(14.0)
703 .set_text_rise(0.5)
704 .set_rendering_mode(TextRenderingMode::FillStroke);
705
706 let ops = context.generate_text_state_operations();
707 assert!(ops.contains("1.50 Tc"));
708 assert!(ops.contains("2.00 Tw"));
709 assert!(ops.contains("110.00 Tz"));
710 assert!(ops.contains("14.00 TL"));
711 assert!(ops.contains("0.50 Ts"));
712 assert!(ops.contains("2 Tr"));
713 }
714
715 #[test]
716 fn test_all_text_state_operators_generated() {
717 let mut context = TextContext::new();
718
719 context.set_character_spacing(1.0); context.set_word_spacing(2.0); context.set_horizontal_scaling(1.2); context.set_leading(15.0); context.set_text_rise(1.0); context.set_rendering_mode(TextRenderingMode::Stroke); let ops = context.generate_text_state_operations();
728
729 assert!(
731 ops.contains("Tc"),
732 "Character spacing operator (Tc) not found"
733 );
734 assert!(ops.contains("Tw"), "Word spacing operator (Tw) not found");
735 assert!(
736 ops.contains("Tz"),
737 "Horizontal scaling operator (Tz) not found"
738 );
739 assert!(ops.contains("TL"), "Leading operator (TL) not found");
740 assert!(ops.contains("Ts"), "Text rise operator (Ts) not found");
741 assert!(
742 ops.contains("Tr"),
743 "Text rendering mode operator (Tr) not found"
744 );
745 }
746
747 #[test]
748 fn test_text_color_operations() {
749 use crate::Color;
750
751 let mut context = TextContext::new();
752
753 context.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
755 context.apply_text_state_parameters();
756
757 let ops = context.operations();
758 assert!(
759 ops.contains("1.000 0.000 0.000 rg"),
760 "RGB fill color operator (rg) not found in: {ops}"
761 );
762
763 context.clear();
765 context.set_stroke_color(Color::rgb(0.0, 1.0, 0.0));
766 context.apply_text_state_parameters();
767
768 let ops = context.operations();
769 assert!(
770 ops.contains("0.000 1.000 0.000 RG"),
771 "RGB stroke color operator (RG) not found in: {ops}"
772 );
773
774 context.clear();
776 context.set_fill_color(Color::gray(0.5));
777 context.apply_text_state_parameters();
778
779 let ops = context.operations();
780 assert!(
781 ops.contains("0.500 g"),
782 "Gray fill color operator (g) not found in: {ops}"
783 );
784
785 context.clear();
787 context.set_stroke_color(Color::cmyk(0.2, 0.3, 0.4, 0.1));
788 context.apply_text_state_parameters();
789
790 let ops = context.operations();
791 assert!(
792 ops.contains("0.200 0.300 0.400 0.100 K"),
793 "CMYK stroke color operator (K) not found in: {ops}"
794 );
795
796 context.clear();
798 context.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
799 context.set_stroke_color(Color::rgb(0.0, 0.0, 1.0));
800 context.apply_text_state_parameters();
801
802 let ops = context.operations();
803 assert!(
804 ops.contains("1.000 0.000 0.000 rg") && ops.contains("0.000 0.000 1.000 RG"),
805 "Both fill and stroke colors not found in: {ops}"
806 );
807 }
808
809 #[test]
811 fn test_used_characters_tracking_ascii() {
812 let mut context = TextContext::new();
813 context.write("Hello").unwrap();
814
815 let chars = context.get_used_characters();
816 assert!(chars.is_some());
817 let chars = chars.unwrap();
818 assert!(chars.contains(&'H'));
819 assert!(chars.contains(&'e'));
820 assert!(chars.contains(&'l'));
821 assert!(chars.contains(&'o'));
822 assert_eq!(chars.len(), 4); }
824
825 #[test]
826 fn test_used_characters_tracking_cjk() {
827 let mut context = TextContext::new();
828 context.set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
829 context.write("中文测试").unwrap();
830
831 let chars = context.get_used_characters();
832 assert!(chars.is_some());
833 let chars = chars.unwrap();
834 assert!(chars.contains(&'中'));
835 assert!(chars.contains(&'文'));
836 assert!(chars.contains(&'测'));
837 assert!(chars.contains(&'试'));
838 assert_eq!(chars.len(), 4);
839 }
840
841 #[test]
842 fn test_used_characters_empty_initially() {
843 let context = TextContext::new();
844 assert!(context.get_used_characters().is_none());
845 }
846
847 #[test]
848 fn test_used_characters_multiple_writes() {
849 let mut context = TextContext::new();
850 context.write("AB").unwrap();
851 context.write("CD").unwrap();
852
853 let chars = context.get_used_characters();
854 assert!(chars.is_some());
855 let chars = chars.unwrap();
856 assert!(chars.contains(&'A'));
857 assert!(chars.contains(&'B'));
858 assert!(chars.contains(&'C'));
859 assert!(chars.contains(&'D'));
860 assert_eq!(chars.len(), 4);
861 }
862}