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