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