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