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