1pub mod cid_to_unicode;
2pub mod cmap;
3pub(crate) mod encoding;
4pub(crate) mod encoding_cmap;
5pub mod extraction;
6mod extraction_cmap;
7pub(crate) mod flat_reading_order;
8mod flow;
9mod font;
10pub mod font_manager;
11pub mod fonts;
12pub(crate) mod graphics_state_stack;
13mod header_footer;
14pub mod invoice;
15mod layout;
16mod list;
17pub mod metrics;
18pub mod ocr;
19pub mod plaintext;
20pub mod structured;
21pub mod table;
22pub mod table_detection;
23pub mod text_block;
24pub mod validation;
25
26#[cfg(test)]
27mod cmap_tests;
28
29#[cfg(test)]
30mod flat_reading_order_tests;
31
32#[cfg(feature = "ocr-tesseract")]
33pub mod tesseract_provider;
34
35pub use encoding::{escape_pdf_string_literal, TextEncoding};
36pub use extraction::{
37 sanitize_extracted_text, sanitize_extracted_text_with_policy, CarriageReturnHandling,
38 ExtractedText, ExtractionOptions, TextExtractor, TextFragment,
39};
40pub use flow::{TextAlign, TextFlowContext};
41pub use font::{Font, FontEncoding, FontFamily, FontWithEncoding};
42pub use font_manager::{CustomFont, FontDescriptor, FontFlags, FontManager, FontMetrics, FontType};
43pub use header_footer::{HeaderFooter, HeaderFooterOptions, HeaderFooterPosition};
44pub use layout::{ColumnContent, ColumnLayout, ColumnOptions, TextFormat};
45pub use list::{
46 BulletStyle, ListElement, ListItem, ListOptions, ListStyle as ListStyleEnum, OrderedList,
47 OrderedListStyle, UnorderedList,
48};
49pub use metrics::{
50 measure_char, measure_char_with, measure_text, measure_text_with, split_into_words,
51 FontMetricsStore,
52};
53pub use ocr::{
54 CharacterConfidence, CorrectionCandidate, CorrectionReason, CorrectionSuggestion,
55 CorrectionType, FragmentType, ImagePreprocessing, MockOcrProvider, OcrEngine, OcrError,
56 OcrOptions, OcrPostProcessor, OcrProcessingResult, OcrProvider, OcrRegion, OcrResult,
57 OcrTextFragment, WordConfidence,
58};
59pub use plaintext::{LineBreakMode, PlainTextConfig, PlainTextExtractor, PlainTextResult};
60pub use table::{HeaderStyle, Table, TableCell, TableOptions};
61pub use text_block::{
62 compute_line_widths, measure_text_block, measure_text_block_with, TextBlockMetrics,
63};
64pub use validation::{MatchType, TextMatch, TextValidationResult, TextValidator};
65
66#[cfg(feature = "ocr-tesseract")]
67pub use tesseract_provider::{RustyTesseractConfig, RustyTesseractProvider};
68
69use crate::error::Result;
70use crate::Color;
71use std::collections::{HashMap, HashSet};
72
73#[derive(Clone, Copy, Debug, PartialEq, Eq)]
77pub enum TextRenderingMode {
78 Fill = 0,
80 Stroke = 1,
82 FillStroke = 2,
84 Invisible = 3,
86 FillClip = 4,
88 StrokeClip = 5,
90 FillStrokeClip = 6,
92 Clip = 7,
94}
95
96pub(crate) fn build_show_text_op(text: &str, font: &Font) -> crate::graphics::ops::Op {
110 use crate::graphics::ops::Op;
111
112 match font {
113 Font::Custom(_) => {
114 let utf16_units: Vec<u16> = text.encode_utf16().collect();
115 let mut hex = String::with_capacity(utf16_units.len() * 4);
116 for unit in utf16_units {
117 use std::fmt::Write as _;
118 write!(
119 &mut hex,
120 "{:02X}{:02X}",
121 (unit >> 8) as u8,
122 (unit & 0xFF) as u8
123 )
124 .expect("write to String never fails");
125 }
126 Op::ShowTextHex(hex.into_bytes())
127 }
128 _ => {
129 let encoded = TextEncoding::WinAnsiEncoding.encode(text);
130 Op::ShowText(encoding::escape_show_text_literal_bytes(&encoded))
131 }
132 }
133}
134
135#[derive(Clone)]
136pub struct TextContext {
137 operations: Vec<crate::graphics::ops::Op>,
138 current_font: Font,
139 font_size: f64,
140 text_matrix: [f64; 6],
141 pending_position: Option<(f64, f64)>,
143 character_spacing: Option<f64>,
145 word_spacing: Option<f64>,
146 horizontal_scaling: Option<f64>,
147 leading: Option<f64>,
148 text_rise: Option<f64>,
149 rendering_mode: Option<TextRenderingMode>,
150 fill_color: Option<Color>,
152 stroke_color: Option<Color>,
153 used_characters_by_font: HashMap<String, HashSet<char>>,
160 #[allow(dead_code)]
164 pub(crate) font_metrics_store: Option<FontMetricsStore>,
165}
166
167impl Default for TextContext {
168 fn default() -> Self {
169 Self::new()
170 }
171}
172
173impl TextContext {
174 pub fn new() -> Self {
175 Self {
176 operations: Vec::new(),
177 current_font: Font::Helvetica,
178 font_size: 12.0,
179 text_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
180 pending_position: None,
181 character_spacing: None,
182 word_spacing: None,
183 horizontal_scaling: None,
184 leading: None,
185 text_rise: None,
186 rendering_mode: None,
187 fill_color: None,
188 stroke_color: None,
189 used_characters_by_font: HashMap::new(),
190 font_metrics_store: None,
191 }
192 }
193
194 pub(crate) fn with_metrics_store(store: Option<FontMetricsStore>) -> Self {
200 let mut ctx = Self::default();
201 ctx.font_metrics_store = store;
202 ctx
203 }
204
205 pub(crate) fn set_metrics_store(&mut self, store: Option<FontMetricsStore>) {
214 self.font_metrics_store = store;
215 }
216
217 fn record_used_chars(&mut self, text: &str) {
222 let name = match &self.current_font {
223 Font::Custom(name) => name.clone(),
224 builtin => builtin.pdf_name(),
225 };
226 self.used_characters_by_font
227 .entry(name)
228 .or_default()
229 .extend(text.chars());
230 }
231
232 #[cfg(test)]
234 pub(crate) fn font_metrics_store_for_test(&self) -> Option<&FontMetricsStore> {
235 self.font_metrics_store.as_ref()
236 }
237
238 #[cfg(test)]
243 pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
244 let merged: HashSet<char> = self
245 .used_characters_by_font
246 .values()
247 .flat_map(|s| s.iter().copied())
248 .collect();
249 if merged.is_empty() {
250 None
251 } else {
252 Some(merged)
253 }
254 }
255
256 pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
258 &self.used_characters_by_font
259 }
260
261 pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
262 self.current_font = font;
263 self.font_size = size;
264 self
265 }
266
267 #[allow(dead_code)]
269 pub(crate) fn current_font(&self) -> &Font {
270 &self.current_font
271 }
272
273 pub(crate) fn fill_color(&self) -> Option<Color> {
277 self.fill_color
278 }
279
280 pub(crate) fn character_spacing(&self) -> Option<f64> {
285 self.character_spacing
286 }
287 pub(crate) fn word_spacing(&self) -> Option<f64> {
288 self.word_spacing
289 }
290 pub(crate) fn horizontal_scaling(&self) -> Option<f64> {
291 self.horizontal_scaling
292 }
293 pub(crate) fn leading(&self) -> Option<f64> {
294 self.leading
295 }
296 pub(crate) fn text_rise(&self) -> Option<f64> {
297 self.text_rise
298 }
299 pub(crate) fn rendering_mode(&self) -> Option<TextRenderingMode> {
300 self.rendering_mode
301 }
302 pub(crate) fn stroke_color(&self) -> Option<Color> {
303 self.stroke_color
304 }
305
306 pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
307 self.text_matrix[4] = x;
309 self.text_matrix[5] = y;
310 self.pending_position = Some((x, y));
311 self
312 }
313
314 pub fn write(&mut self, text: &str) -> Result<&mut Self> {
315 use crate::graphics::ops::Op;
316
317 self.operations.push(Op::BeginText);
318
319 self.operations.push(Op::SetFont {
321 name: self.current_font.pdf_name(),
322 size: self.font_size,
323 });
324
325 self.apply_text_state_parameters();
327
328 let (x, y) = if let Some((px, py)) = self.pending_position.take() {
330 (px, py)
331 } else {
332 (self.text_matrix[4], self.text_matrix[5])
333 };
334 self.operations.push(Op::SetTextPosition { x, y });
335
336 self.operations
341 .push(build_show_text_op(text, &self.current_font));
342
343 self.record_used_chars(text);
346
347 self.operations.push(Op::EndText);
348
349 Ok(self)
350 }
351
352 pub fn write_line(&mut self, text: &str) -> Result<&mut Self> {
353 self.write(text)?;
354 self.text_matrix[5] -= self.font_size * 1.2; Ok(self)
356 }
357
358 pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
359 self.character_spacing = Some(spacing);
360 self
361 }
362
363 pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
364 self.word_spacing = Some(spacing);
365 self
366 }
367
368 pub fn set_horizontal_scaling(&mut self, scale: f64) -> &mut Self {
369 self.horizontal_scaling = Some(scale);
370 self
371 }
372
373 pub fn set_leading(&mut self, leading: f64) -> &mut Self {
374 self.leading = Some(leading);
375 self
376 }
377
378 pub fn set_text_rise(&mut self, rise: f64) -> &mut Self {
379 self.text_rise = Some(rise);
380 self
381 }
382
383 pub fn set_rendering_mode(&mut self, mode: TextRenderingMode) -> &mut Self {
385 self.rendering_mode = Some(mode);
386 self
387 }
388
389 pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
391 self.fill_color = Some(color);
392 self
393 }
394
395 pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
397 self.stroke_color = Some(color);
398 self
399 }
400
401 fn apply_text_state_parameters(&mut self) {
407 use crate::graphics::ops::Op;
408
409 if let Some(spacing) = self.character_spacing {
410 self.operations.push(Op::SetCharSpacing(spacing));
411 }
412 if let Some(spacing) = self.word_spacing {
413 self.operations.push(Op::SetWordSpacing(spacing));
414 }
415 if let Some(scale) = self.horizontal_scaling {
416 self.operations
420 .push(Op::SetHorizontalScaling(scale * 100.0));
421 }
422 if let Some(leading) = self.leading {
423 self.operations.push(Op::SetLeading(leading));
424 }
425 if let Some(rise) = self.text_rise {
426 self.operations.push(Op::SetTextRise(rise));
427 }
428 if let Some(mode) = self.rendering_mode {
429 self.operations.push(Op::SetRenderingMode(mode as u8));
430 }
431
432 if let Some(color) = self.fill_color {
436 self.operations.push(Op::SetFillColor(color));
437 }
438 if let Some(color) = self.stroke_color {
439 self.operations.push(Op::SetStrokeColor(color));
440 }
441 }
442
443 pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
444 let mut buf = Vec::new();
445 crate::graphics::ops::serialize_ops(&mut buf, &self.operations);
446 Ok(buf)
447 }
448
449 pub(crate) fn drain_ops(&mut self) -> Vec<crate::graphics::ops::Op> {
454 std::mem::take(&mut self.operations)
455 }
456
457 pub(crate) fn ops_slice(&self) -> &[crate::graphics::ops::Op] {
459 &self.operations
460 }
461
462 pub(crate) fn append_raw_operation(&mut self, operation: &str) {
467 self.operations
468 .push(crate::graphics::ops::Op::Raw(operation.as_bytes().to_vec()));
469 }
470
471 pub fn font_size(&self) -> f64 {
473 self.font_size
474 }
475
476 pub fn text_matrix(&self) -> [f64; 6] {
478 self.text_matrix
479 }
480
481 pub fn position(&self) -> (f64, f64) {
483 (self.text_matrix[4], self.text_matrix[5])
484 }
485
486 pub fn clear(&mut self) {
488 self.operations.clear();
489 self.character_spacing = None;
490 self.word_spacing = None;
491 self.horizontal_scaling = None;
492 self.leading = None;
493 self.text_rise = None;
494 self.rendering_mode = None;
495 self.fill_color = None;
496 self.stroke_color = None;
497 }
498
499 pub fn operations(&self) -> String {
506 crate::graphics::ops::ops_to_string(&self.operations)
507 }
508
509 #[cfg(test)]
512 pub fn generate_text_state_operations(&self) -> String {
513 use crate::graphics::ops::{ops_to_string, Op};
514
515 let mut ops = Vec::new();
516 if let Some(spacing) = self.character_spacing {
517 ops.push(Op::SetCharSpacing(spacing));
518 }
519 if let Some(spacing) = self.word_spacing {
520 ops.push(Op::SetWordSpacing(spacing));
521 }
522 if let Some(scale) = self.horizontal_scaling {
523 ops.push(Op::SetHorizontalScaling(scale * 100.0));
524 }
525 if let Some(leading) = self.leading {
526 ops.push(Op::SetLeading(leading));
527 }
528 if let Some(rise) = self.text_rise {
529 ops.push(Op::SetTextRise(rise));
530 }
531 if let Some(mode) = self.rendering_mode {
532 ops.push(Op::SetRenderingMode(mode as u8));
533 }
534 ops_to_string(&ops)
535 }
536}
537
538#[cfg(test)]
539mod tests {
540 use super::*;
541
542 #[test]
543 fn test_text_context_new() {
544 let context = TextContext::new();
545 assert_eq!(context.current_font, Font::Helvetica);
546 assert_eq!(context.font_size, 12.0);
547 assert_eq!(context.text_matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
548 assert!(context.operations.is_empty());
549 }
550
551 #[test]
552 fn test_text_context_default() {
553 let context = TextContext::default();
554 assert_eq!(context.current_font, Font::Helvetica);
555 assert_eq!(context.font_size, 12.0);
556 }
557
558 #[test]
559 fn test_set_font() {
560 let mut context = TextContext::new();
561 context.set_font(Font::TimesBold, 14.0);
562 assert_eq!(context.current_font, Font::TimesBold);
563 assert_eq!(context.font_size, 14.0);
564 }
565
566 #[test]
567 fn test_position() {
568 let mut context = TextContext::new();
569 context.at(100.0, 200.0);
570 let (x, y) = context.position();
571 assert_eq!(x, 100.0);
572 assert_eq!(y, 200.0);
573 assert_eq!(context.text_matrix[4], 100.0);
574 assert_eq!(context.text_matrix[5], 200.0);
575 }
576
577 #[test]
578 fn test_write_simple_text() {
579 let mut context = TextContext::new();
580 context.write("Hello").unwrap();
581
582 let ops = context.operations();
583 assert!(ops.contains("BT\n"));
584 assert!(ops.contains("ET\n"));
585 assert!(ops.contains("/Helvetica 12 Tf"));
586 assert!(ops.contains("(Hello) Tj"));
587 }
588
589 #[test]
590 fn test_write_text_with_escaping() {
591 let mut context = TextContext::new();
592 context.write("(Hello)").unwrap();
593
594 let ops = context.operations();
595 assert!(ops.contains("(\\(Hello\\)) Tj"));
596 }
597
598 #[test]
599 fn test_write_line() {
600 let mut context = TextContext::new();
601 let initial_y = context.text_matrix[5];
602 context.write_line("Line 1").unwrap();
603
604 let new_y = context.text_matrix[5];
606 assert!(new_y < initial_y);
607 assert_eq!(new_y, initial_y - 12.0 * 1.2); }
609
610 #[test]
611 fn test_character_spacing() {
612 let mut context = TextContext::new();
613 context.set_character_spacing(2.5);
614
615 let ops = context.generate_text_state_operations();
616 assert!(ops.contains("2.50 Tc"));
617 }
618
619 #[test]
620 fn test_word_spacing() {
621 let mut context = TextContext::new();
622 context.set_word_spacing(1.5);
623
624 let ops = context.generate_text_state_operations();
625 assert!(ops.contains("1.50 Tw"));
626 }
627
628 #[test]
629 fn test_horizontal_scaling() {
630 let mut context = TextContext::new();
631 context.set_horizontal_scaling(1.25);
632
633 let ops = context.generate_text_state_operations();
634 assert!(ops.contains("125.00 Tz")); }
636
637 #[test]
638 fn test_leading() {
639 let mut context = TextContext::new();
640 context.set_leading(15.0);
641
642 let ops = context.generate_text_state_operations();
643 assert!(ops.contains("15.00 TL"));
644 }
645
646 #[test]
647 fn test_text_rise() {
648 let mut context = TextContext::new();
649 context.set_text_rise(3.0);
650
651 let ops = context.generate_text_state_operations();
652 assert!(ops.contains("3.00 Ts"));
653 }
654
655 #[test]
656 fn test_clear() {
657 let mut context = TextContext::new();
658 context.write("Hello").unwrap();
659 assert!(!context.operations().is_empty());
660
661 context.clear();
662 assert!(context.operations().is_empty());
663 }
664
665 #[test]
666 fn test_generate_operations() {
667 let mut context = TextContext::new();
668 context.write("Test").unwrap();
669
670 let ops_bytes = context.generate_operations().unwrap();
671 let ops_string = String::from_utf8(ops_bytes).unwrap();
672 assert_eq!(ops_string, context.operations());
673 }
674
675 #[test]
676 fn test_method_chaining() {
677 let mut context = TextContext::new();
678 context
679 .set_font(Font::Courier, 10.0)
680 .at(50.0, 100.0)
681 .set_character_spacing(1.0)
682 .set_word_spacing(2.0);
683
684 assert_eq!(context.current_font(), &Font::Courier);
685 assert_eq!(context.font_size(), 10.0);
686 let (x, y) = context.position();
687 assert_eq!(x, 50.0);
688 assert_eq!(y, 100.0);
689 }
690
691 #[test]
692 fn test_text_matrix_access() {
693 let mut context = TextContext::new();
694 context.at(25.0, 75.0);
695
696 let matrix = context.text_matrix();
697 assert_eq!(matrix, [1.0, 0.0, 0.0, 1.0, 25.0, 75.0]);
698 }
699
700 #[test]
701 fn test_special_characters_encoding() {
702 let mut context = TextContext::new();
703 context.write("Test\nLine\tTab").unwrap();
704
705 let ops = context.operations();
706 assert!(ops.contains("\\n"));
707 assert!(ops.contains("\\t"));
708 }
709
710 #[test]
711 fn test_rendering_mode_fill() {
712 let mut context = TextContext::new();
713 context.set_rendering_mode(TextRenderingMode::Fill);
714
715 let ops = context.generate_text_state_operations();
716 assert!(ops.contains("0 Tr"));
717 }
718
719 #[test]
720 fn test_rendering_mode_stroke() {
721 let mut context = TextContext::new();
722 context.set_rendering_mode(TextRenderingMode::Stroke);
723
724 let ops = context.generate_text_state_operations();
725 assert!(ops.contains("1 Tr"));
726 }
727
728 #[test]
729 fn test_rendering_mode_fill_stroke() {
730 let mut context = TextContext::new();
731 context.set_rendering_mode(TextRenderingMode::FillStroke);
732
733 let ops = context.generate_text_state_operations();
734 assert!(ops.contains("2 Tr"));
735 }
736
737 #[test]
738 fn test_rendering_mode_invisible() {
739 let mut context = TextContext::new();
740 context.set_rendering_mode(TextRenderingMode::Invisible);
741
742 let ops = context.generate_text_state_operations();
743 assert!(ops.contains("3 Tr"));
744 }
745
746 #[test]
747 fn test_rendering_mode_fill_clip() {
748 let mut context = TextContext::new();
749 context.set_rendering_mode(TextRenderingMode::FillClip);
750
751 let ops = context.generate_text_state_operations();
752 assert!(ops.contains("4 Tr"));
753 }
754
755 #[test]
756 fn test_rendering_mode_stroke_clip() {
757 let mut context = TextContext::new();
758 context.set_rendering_mode(TextRenderingMode::StrokeClip);
759
760 let ops = context.generate_text_state_operations();
761 assert!(ops.contains("5 Tr"));
762 }
763
764 #[test]
765 fn test_rendering_mode_fill_stroke_clip() {
766 let mut context = TextContext::new();
767 context.set_rendering_mode(TextRenderingMode::FillStrokeClip);
768
769 let ops = context.generate_text_state_operations();
770 assert!(ops.contains("6 Tr"));
771 }
772
773 #[test]
774 fn test_rendering_mode_clip() {
775 let mut context = TextContext::new();
776 context.set_rendering_mode(TextRenderingMode::Clip);
777
778 let ops = context.generate_text_state_operations();
779 assert!(ops.contains("7 Tr"));
780 }
781
782 #[test]
783 fn test_text_state_parameters_chaining() {
784 let mut context = TextContext::new();
785 context
786 .set_character_spacing(1.5)
787 .set_word_spacing(2.0)
788 .set_horizontal_scaling(1.1)
789 .set_leading(14.0)
790 .set_text_rise(0.5)
791 .set_rendering_mode(TextRenderingMode::FillStroke);
792
793 let ops = context.generate_text_state_operations();
794 assert!(ops.contains("1.50 Tc"));
795 assert!(ops.contains("2.00 Tw"));
796 assert!(ops.contains("110.00 Tz"));
797 assert!(ops.contains("14.00 TL"));
798 assert!(ops.contains("0.50 Ts"));
799 assert!(ops.contains("2 Tr"));
800 }
801
802 #[test]
803 fn test_all_text_state_operators_generated() {
804 let mut context = TextContext::new();
805
806 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();
815
816 assert!(
818 ops.contains("Tc"),
819 "Character spacing operator (Tc) not found"
820 );
821 assert!(ops.contains("Tw"), "Word spacing operator (Tw) not found");
822 assert!(
823 ops.contains("Tz"),
824 "Horizontal scaling operator (Tz) not found"
825 );
826 assert!(ops.contains("TL"), "Leading operator (TL) not found");
827 assert!(ops.contains("Ts"), "Text rise operator (Ts) not found");
828 assert!(
829 ops.contains("Tr"),
830 "Text rendering mode operator (Tr) not found"
831 );
832 }
833
834 #[test]
835 fn test_text_color_operations() {
836 use crate::Color;
837
838 let mut context = TextContext::new();
839
840 context.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
842 context.apply_text_state_parameters();
843
844 let ops = context.operations();
845 assert!(
846 ops.contains("1.000 0.000 0.000 rg"),
847 "RGB fill color operator (rg) not found in: {ops}"
848 );
849
850 context.clear();
852 context.set_stroke_color(Color::rgb(0.0, 1.0, 0.0));
853 context.apply_text_state_parameters();
854
855 let ops = context.operations();
856 assert!(
857 ops.contains("0.000 1.000 0.000 RG"),
858 "RGB stroke color operator (RG) not found in: {ops}"
859 );
860
861 context.clear();
863 context.set_fill_color(Color::gray(0.5));
864 context.apply_text_state_parameters();
865
866 let ops = context.operations();
867 assert!(
868 ops.contains("0.500 g"),
869 "Gray fill color operator (g) not found in: {ops}"
870 );
871
872 context.clear();
874 context.set_stroke_color(Color::cmyk(0.2, 0.3, 0.4, 0.1));
875 context.apply_text_state_parameters();
876
877 let ops = context.operations();
878 assert!(
879 ops.contains("0.200 0.300 0.400 0.100 K"),
880 "CMYK stroke color operator (K) not found in: {ops}"
881 );
882
883 context.clear();
885 context.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
886 context.set_stroke_color(Color::rgb(0.0, 0.0, 1.0));
887 context.apply_text_state_parameters();
888
889 let ops = context.operations();
890 assert!(
891 ops.contains("1.000 0.000 0.000 rg") && ops.contains("0.000 0.000 1.000 RG"),
892 "Both fill and stroke colors not found in: {ops}"
893 );
894 }
895
896 #[test]
898 fn test_used_characters_tracking_ascii() {
899 let mut context = TextContext::new();
900 context.write("Hello").unwrap();
901
902 let chars = context.get_used_characters();
903 assert!(chars.is_some());
904 let chars = chars.unwrap();
905 assert!(chars.contains(&'H'));
906 assert!(chars.contains(&'e'));
907 assert!(chars.contains(&'l'));
908 assert!(chars.contains(&'o'));
909 assert_eq!(chars.len(), 4); }
911
912 #[test]
913 fn test_used_characters_tracking_cjk() {
914 let mut context = TextContext::new();
915 context.set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
916 context.write("中文测试").unwrap();
917
918 let chars = context.get_used_characters();
919 assert!(chars.is_some());
920 let chars = chars.unwrap();
921 assert!(chars.contains(&'中'));
922 assert!(chars.contains(&'文'));
923 assert!(chars.contains(&'测'));
924 assert!(chars.contains(&'试'));
925 assert_eq!(chars.len(), 4);
926 }
927
928 #[test]
929 fn test_used_characters_empty_initially() {
930 let context = TextContext::new();
931 assert!(context.get_used_characters().is_none());
932 }
933
934 #[test]
935 fn test_used_characters_multiple_writes() {
936 let mut context = TextContext::new();
937 context.write("AB").unwrap();
938 context.write("CD").unwrap();
939
940 let chars = context.get_used_characters();
941 assert!(chars.is_some());
942 let chars = chars.unwrap();
943 assert!(chars.contains(&'A'));
944 assert!(chars.contains(&'B'));
945 assert!(chars.contains(&'C'));
946 assert!(chars.contains(&'D'));
947 assert_eq!(chars.len(), 4);
948 }
949
950 #[test]
956 fn nan_char_spacing_sanitised_at_emission() {
957 let mut ctx = TextContext::new();
958 ctx.set_character_spacing(f64::NAN);
959 ctx.write("hi").unwrap();
960 let ops = ctx.operations();
961 assert!(
962 ops.contains("0.00 Tc\n"),
963 "NaN char spacing must emit `0.00 Tc`, got: {ops:?}"
964 );
965 assert!(
966 !ops.contains("NaN") && !ops.contains("inf"),
967 "non-finite tokens must not appear in any Tc/Tw/Tz/TL/Ts emission, got: {ops:?}"
968 );
969 }
970
971 #[test]
972 fn pos_inf_word_spacing_sanitised_at_emission() {
973 let mut ctx = TextContext::new();
974 ctx.set_word_spacing(f64::INFINITY);
975 ctx.write("hi").unwrap();
976 let ops = ctx.operations();
977 assert!(
978 ops.contains("0.00 Tw\n"),
979 "+inf word spacing must emit `0.00 Tw`, got: {ops:?}"
980 );
981 assert!(
982 !ops.contains("inf"),
983 "`inf` must not appear in Tw output, got: {ops:?}"
984 );
985 }
986
987 #[test]
988 fn nan_horizontal_scaling_sanitised_at_emission() {
989 let mut ctx = TextContext::new();
990 ctx.set_horizontal_scaling(f64::NAN);
991 ctx.write("hi").unwrap();
992 let ops = ctx.operations();
993 assert!(
994 ops.contains("0.00 Tz\n"),
995 "NaN horizontal scaling must emit `0.00 Tz`, got: {ops:?}"
996 );
997 }
998
999 #[test]
1000 fn nan_leading_and_text_rise_sanitised_at_emission() {
1001 let mut ctx = TextContext::new();
1002 ctx.set_leading(f64::NEG_INFINITY);
1003 ctx.set_text_rise(f64::NAN);
1004 ctx.write("hi").unwrap();
1005 let ops = ctx.operations();
1006 assert!(
1007 ops.contains("0.00 TL\n"),
1008 "-inf leading must emit `0.00 TL`, got: {ops:?}"
1009 );
1010 assert!(
1011 ops.contains("0.00 Ts\n"),
1012 "NaN text rise must emit `0.00 Ts`, got: {ops:?}"
1013 );
1014 }
1015
1016 #[test]
1017 fn test_text_context_threads_metrics_store() {
1018 use crate::text::metrics::{FontMetrics, FontMetricsStore};
1019 let store = FontMetricsStore::new();
1020 let ctx = TextContext::with_metrics_store(Some(store.clone()));
1021 assert!(ctx.font_metrics_store_for_test().is_some());
1023 store.register("X", FontMetrics::new(400));
1025 assert_eq!(
1026 ctx.font_metrics_store_for_test().unwrap().len(),
1027 1,
1028 "TextContext must hold a clone that shares the underlying registry"
1029 );
1030 }
1031}