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