pub mod cid_to_unicode;
pub mod cmap;
mod encoding;
pub mod extraction;
mod extraction_cmap;
mod flow;
mod font;
pub mod font_manager;
pub mod fonts;
mod header_footer;
pub mod invoice;
mod layout;
mod list;
pub mod metrics;
pub mod ocr;
pub mod plaintext;
pub mod structured;
pub mod table;
pub mod table_detection;
pub mod text_block;
pub mod validation;
#[cfg(test)]
mod cmap_tests;
#[cfg(feature = "ocr-tesseract")]
pub mod tesseract_provider;
pub use encoding::{escape_pdf_string_literal, TextEncoding};
pub use extraction::{
sanitize_extracted_text, ExtractedText, ExtractionOptions, TextExtractor, TextFragment,
};
pub use flow::{TextAlign, TextFlowContext};
pub use font::{Font, FontEncoding, FontFamily, FontWithEncoding};
pub use font_manager::{CustomFont, FontDescriptor, FontFlags, FontManager, FontMetrics, FontType};
pub use header_footer::{HeaderFooter, HeaderFooterOptions, HeaderFooterPosition};
pub use layout::{ColumnContent, ColumnLayout, ColumnOptions, TextFormat};
pub use list::{
BulletStyle, ListElement, ListItem, ListOptions, ListStyle as ListStyleEnum, OrderedList,
OrderedListStyle, UnorderedList,
};
pub use metrics::{
measure_char, measure_char_with, measure_text, measure_text_with, split_into_words,
FontMetricsStore,
};
pub use ocr::{
CharacterConfidence, CorrectionCandidate, CorrectionReason, CorrectionSuggestion,
CorrectionType, FragmentType, ImagePreprocessing, MockOcrProvider, OcrEngine, OcrError,
OcrOptions, OcrPostProcessor, OcrProcessingResult, OcrProvider, OcrRegion, OcrResult,
OcrTextFragment, WordConfidence,
};
pub use plaintext::{LineBreakMode, PlainTextConfig, PlainTextExtractor, PlainTextResult};
pub use table::{HeaderStyle, Table, TableCell, TableOptions};
pub use text_block::{
compute_line_widths, measure_text_block, measure_text_block_with, TextBlockMetrics,
};
pub use validation::{MatchType, TextMatch, TextValidationResult, TextValidator};
#[cfg(feature = "ocr-tesseract")]
pub use tesseract_provider::{RustyTesseractConfig, RustyTesseractProvider};
use crate::error::Result;
use crate::Color;
use std::collections::{HashMap, HashSet};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum TextRenderingMode {
Fill = 0,
Stroke = 1,
FillStroke = 2,
Invisible = 3,
FillClip = 4,
StrokeClip = 5,
FillStrokeClip = 6,
Clip = 7,
}
#[derive(Clone)]
pub struct TextContext {
operations: Vec<crate::graphics::ops::Op>,
current_font: Font,
font_size: f64,
text_matrix: [f64; 6],
pending_position: Option<(f64, f64)>,
character_spacing: Option<f64>,
word_spacing: Option<f64>,
horizontal_scaling: Option<f64>,
leading: Option<f64>,
text_rise: Option<f64>,
rendering_mode: Option<TextRenderingMode>,
fill_color: Option<Color>,
stroke_color: Option<Color>,
used_characters_by_font: HashMap<String, HashSet<char>>,
#[allow(dead_code)]
pub(crate) font_metrics_store: Option<FontMetricsStore>,
}
impl Default for TextContext {
fn default() -> Self {
Self::new()
}
}
impl TextContext {
pub fn new() -> Self {
Self {
operations: Vec::new(),
current_font: Font::Helvetica,
font_size: 12.0,
text_matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
pending_position: None,
character_spacing: None,
word_spacing: None,
horizontal_scaling: None,
leading: None,
text_rise: None,
rendering_mode: None,
fill_color: None,
stroke_color: None,
used_characters_by_font: HashMap::new(),
font_metrics_store: None,
}
}
#[allow(dead_code)]
pub(crate) fn with_metrics_store(store: Option<FontMetricsStore>) -> Self {
let mut ctx = Self::default();
ctx.font_metrics_store = store;
ctx
}
fn record_used_chars(&mut self, text: &str) {
let name = match &self.current_font {
Font::Custom(name) => name.clone(),
builtin => builtin.pdf_name(),
};
self.used_characters_by_font
.entry(name)
.or_default()
.extend(text.chars());
}
#[cfg(test)]
pub(crate) fn font_metrics_store_for_test(&self) -> Option<&FontMetricsStore> {
self.font_metrics_store.as_ref()
}
#[cfg(test)]
pub(crate) fn get_used_characters(&self) -> Option<HashSet<char>> {
let merged: HashSet<char> = self
.used_characters_by_font
.values()
.flat_map(|s| s.iter().copied())
.collect();
if merged.is_empty() {
None
} else {
Some(merged)
}
}
pub(crate) fn get_used_characters_by_font(&self) -> &HashMap<String, HashSet<char>> {
&self.used_characters_by_font
}
pub fn set_font(&mut self, font: Font, size: f64) -> &mut Self {
self.current_font = font;
self.font_size = size;
self
}
#[allow(dead_code)]
pub(crate) fn current_font(&self) -> &Font {
&self.current_font
}
pub(crate) fn fill_color(&self) -> Option<Color> {
self.fill_color
}
pub(crate) fn character_spacing(&self) -> Option<f64> {
self.character_spacing
}
pub(crate) fn word_spacing(&self) -> Option<f64> {
self.word_spacing
}
pub(crate) fn horizontal_scaling(&self) -> Option<f64> {
self.horizontal_scaling
}
pub(crate) fn leading(&self) -> Option<f64> {
self.leading
}
pub(crate) fn text_rise(&self) -> Option<f64> {
self.text_rise
}
pub(crate) fn rendering_mode(&self) -> Option<TextRenderingMode> {
self.rendering_mode
}
pub(crate) fn stroke_color(&self) -> Option<Color> {
self.stroke_color
}
pub fn at(&mut self, x: f64, y: f64) -> &mut Self {
self.text_matrix[4] = x;
self.text_matrix[5] = y;
self.pending_position = Some((x, y));
self
}
pub fn write(&mut self, text: &str) -> Result<&mut Self> {
use crate::graphics::ops::Op;
self.operations.push(Op::BeginText);
self.operations.push(Op::SetFont {
name: self.current_font.pdf_name(),
size: self.font_size,
});
self.apply_text_state_parameters();
let (x, y) = if let Some((px, py)) = self.pending_position.take() {
(px, py)
} else {
(self.text_matrix[4], self.text_matrix[5])
};
self.operations.push(Op::SetTextPosition { x, y });
match &self.current_font {
Font::Custom(_) => {
let utf16_units: Vec<u16> = text.encode_utf16().collect();
let mut hex = String::new();
for unit in utf16_units {
use std::fmt::Write as _;
write!(
&mut hex,
"{:02X}{:02X}",
(unit >> 8) as u8,
(unit & 0xFF) as u8
)
.expect("write to String never fails");
}
self.operations.push(Op::ShowTextHex(hex.into_bytes()));
}
_ => {
let encoding = TextEncoding::WinAnsiEncoding;
let encoded_bytes = encoding.encode(text);
let mut buf = Vec::with_capacity(encoded_bytes.len());
for &byte in &encoded_bytes {
match byte {
b'(' => buf.extend_from_slice(b"\\("),
b')' => buf.extend_from_slice(b"\\)"),
b'\\' => buf.extend_from_slice(b"\\\\"),
b'\n' => buf.extend_from_slice(b"\\n"),
b'\r' => buf.extend_from_slice(b"\\r"),
b'\t' => buf.extend_from_slice(b"\\t"),
0x20..=0x7E => buf.push(byte),
_ => {
use std::io::Write as _;
write!(&mut buf, "\\{byte:03o}").expect("write to Vec<u8> never fails");
}
}
}
self.operations.push(Op::ShowText(buf));
}
}
self.record_used_chars(text);
self.operations.push(Op::EndText);
Ok(self)
}
pub fn write_line(&mut self, text: &str) -> Result<&mut Self> {
self.write(text)?;
self.text_matrix[5] -= self.font_size * 1.2; Ok(self)
}
pub fn set_character_spacing(&mut self, spacing: f64) -> &mut Self {
self.character_spacing = Some(spacing);
self
}
pub fn set_word_spacing(&mut self, spacing: f64) -> &mut Self {
self.word_spacing = Some(spacing);
self
}
pub fn set_horizontal_scaling(&mut self, scale: f64) -> &mut Self {
self.horizontal_scaling = Some(scale);
self
}
pub fn set_leading(&mut self, leading: f64) -> &mut Self {
self.leading = Some(leading);
self
}
pub fn set_text_rise(&mut self, rise: f64) -> &mut Self {
self.text_rise = Some(rise);
self
}
pub fn set_rendering_mode(&mut self, mode: TextRenderingMode) -> &mut Self {
self.rendering_mode = Some(mode);
self
}
pub fn set_fill_color(&mut self, color: Color) -> &mut Self {
self.fill_color = Some(color);
self
}
pub fn set_stroke_color(&mut self, color: Color) -> &mut Self {
self.stroke_color = Some(color);
self
}
fn apply_text_state_parameters(&mut self) {
use crate::graphics::ops::Op;
if let Some(spacing) = self.character_spacing {
self.operations.push(Op::SetCharSpacing(spacing));
}
if let Some(spacing) = self.word_spacing {
self.operations.push(Op::SetWordSpacing(spacing));
}
if let Some(scale) = self.horizontal_scaling {
self.operations
.push(Op::SetHorizontalScaling(scale * 100.0));
}
if let Some(leading) = self.leading {
self.operations.push(Op::SetLeading(leading));
}
if let Some(rise) = self.text_rise {
self.operations.push(Op::SetTextRise(rise));
}
if let Some(mode) = self.rendering_mode {
self.operations.push(Op::SetRenderingMode(mode as u8));
}
if let Some(color) = self.fill_color {
self.operations.push(Op::SetFillColor(color));
}
if let Some(color) = self.stroke_color {
self.operations.push(Op::SetStrokeColor(color));
}
}
pub(crate) fn generate_operations(&self) -> Result<Vec<u8>> {
let mut buf = Vec::new();
crate::graphics::ops::serialize_ops(&mut buf, &self.operations);
Ok(buf)
}
pub(crate) fn drain_ops(&mut self) -> Vec<crate::graphics::ops::Op> {
std::mem::take(&mut self.operations)
}
pub(crate) fn ops_slice(&self) -> &[crate::graphics::ops::Op] {
&self.operations
}
pub(crate) fn append_raw_operation(&mut self, operation: &str) {
self.operations
.push(crate::graphics::ops::Op::Raw(operation.as_bytes().to_vec()));
}
pub fn font_size(&self) -> f64 {
self.font_size
}
pub fn text_matrix(&self) -> [f64; 6] {
self.text_matrix
}
pub fn position(&self) -> (f64, f64) {
(self.text_matrix[4], self.text_matrix[5])
}
pub fn clear(&mut self) {
self.operations.clear();
self.character_spacing = None;
self.word_spacing = None;
self.horizontal_scaling = None;
self.leading = None;
self.text_rise = None;
self.rendering_mode = None;
self.fill_color = None;
self.stroke_color = None;
}
pub fn operations(&self) -> String {
crate::graphics::ops::ops_to_string(&self.operations)
}
#[cfg(test)]
pub fn generate_text_state_operations(&self) -> String {
use crate::graphics::ops::{ops_to_string, Op};
let mut ops = Vec::new();
if let Some(spacing) = self.character_spacing {
ops.push(Op::SetCharSpacing(spacing));
}
if let Some(spacing) = self.word_spacing {
ops.push(Op::SetWordSpacing(spacing));
}
if let Some(scale) = self.horizontal_scaling {
ops.push(Op::SetHorizontalScaling(scale * 100.0));
}
if let Some(leading) = self.leading {
ops.push(Op::SetLeading(leading));
}
if let Some(rise) = self.text_rise {
ops.push(Op::SetTextRise(rise));
}
if let Some(mode) = self.rendering_mode {
ops.push(Op::SetRenderingMode(mode as u8));
}
ops_to_string(&ops)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_text_context_new() {
let context = TextContext::new();
assert_eq!(context.current_font, Font::Helvetica);
assert_eq!(context.font_size, 12.0);
assert_eq!(context.text_matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
assert!(context.operations.is_empty());
}
#[test]
fn test_text_context_default() {
let context = TextContext::default();
assert_eq!(context.current_font, Font::Helvetica);
assert_eq!(context.font_size, 12.0);
}
#[test]
fn test_set_font() {
let mut context = TextContext::new();
context.set_font(Font::TimesBold, 14.0);
assert_eq!(context.current_font, Font::TimesBold);
assert_eq!(context.font_size, 14.0);
}
#[test]
fn test_position() {
let mut context = TextContext::new();
context.at(100.0, 200.0);
let (x, y) = context.position();
assert_eq!(x, 100.0);
assert_eq!(y, 200.0);
assert_eq!(context.text_matrix[4], 100.0);
assert_eq!(context.text_matrix[5], 200.0);
}
#[test]
fn test_write_simple_text() {
let mut context = TextContext::new();
context.write("Hello").unwrap();
let ops = context.operations();
assert!(ops.contains("BT\n"));
assert!(ops.contains("ET\n"));
assert!(ops.contains("/Helvetica 12 Tf"));
assert!(ops.contains("(Hello) Tj"));
}
#[test]
fn test_write_text_with_escaping() {
let mut context = TextContext::new();
context.write("(Hello)").unwrap();
let ops = context.operations();
assert!(ops.contains("(\\(Hello\\)) Tj"));
}
#[test]
fn test_write_line() {
let mut context = TextContext::new();
let initial_y = context.text_matrix[5];
context.write_line("Line 1").unwrap();
let new_y = context.text_matrix[5];
assert!(new_y < initial_y);
assert_eq!(new_y, initial_y - 12.0 * 1.2); }
#[test]
fn test_character_spacing() {
let mut context = TextContext::new();
context.set_character_spacing(2.5);
let ops = context.generate_text_state_operations();
assert!(ops.contains("2.50 Tc"));
}
#[test]
fn test_word_spacing() {
let mut context = TextContext::new();
context.set_word_spacing(1.5);
let ops = context.generate_text_state_operations();
assert!(ops.contains("1.50 Tw"));
}
#[test]
fn test_horizontal_scaling() {
let mut context = TextContext::new();
context.set_horizontal_scaling(1.25);
let ops = context.generate_text_state_operations();
assert!(ops.contains("125.00 Tz")); }
#[test]
fn test_leading() {
let mut context = TextContext::new();
context.set_leading(15.0);
let ops = context.generate_text_state_operations();
assert!(ops.contains("15.00 TL"));
}
#[test]
fn test_text_rise() {
let mut context = TextContext::new();
context.set_text_rise(3.0);
let ops = context.generate_text_state_operations();
assert!(ops.contains("3.00 Ts"));
}
#[test]
fn test_clear() {
let mut context = TextContext::new();
context.write("Hello").unwrap();
assert!(!context.operations().is_empty());
context.clear();
assert!(context.operations().is_empty());
}
#[test]
fn test_generate_operations() {
let mut context = TextContext::new();
context.write("Test").unwrap();
let ops_bytes = context.generate_operations().unwrap();
let ops_string = String::from_utf8(ops_bytes).unwrap();
assert_eq!(ops_string, context.operations());
}
#[test]
fn test_method_chaining() {
let mut context = TextContext::new();
context
.set_font(Font::Courier, 10.0)
.at(50.0, 100.0)
.set_character_spacing(1.0)
.set_word_spacing(2.0);
assert_eq!(context.current_font(), &Font::Courier);
assert_eq!(context.font_size(), 10.0);
let (x, y) = context.position();
assert_eq!(x, 50.0);
assert_eq!(y, 100.0);
}
#[test]
fn test_text_matrix_access() {
let mut context = TextContext::new();
context.at(25.0, 75.0);
let matrix = context.text_matrix();
assert_eq!(matrix, [1.0, 0.0, 0.0, 1.0, 25.0, 75.0]);
}
#[test]
fn test_special_characters_encoding() {
let mut context = TextContext::new();
context.write("Test\nLine\tTab").unwrap();
let ops = context.operations();
assert!(ops.contains("\\n"));
assert!(ops.contains("\\t"));
}
#[test]
fn test_rendering_mode_fill() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::Fill);
let ops = context.generate_text_state_operations();
assert!(ops.contains("0 Tr"));
}
#[test]
fn test_rendering_mode_stroke() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::Stroke);
let ops = context.generate_text_state_operations();
assert!(ops.contains("1 Tr"));
}
#[test]
fn test_rendering_mode_fill_stroke() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::FillStroke);
let ops = context.generate_text_state_operations();
assert!(ops.contains("2 Tr"));
}
#[test]
fn test_rendering_mode_invisible() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::Invisible);
let ops = context.generate_text_state_operations();
assert!(ops.contains("3 Tr"));
}
#[test]
fn test_rendering_mode_fill_clip() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::FillClip);
let ops = context.generate_text_state_operations();
assert!(ops.contains("4 Tr"));
}
#[test]
fn test_rendering_mode_stroke_clip() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::StrokeClip);
let ops = context.generate_text_state_operations();
assert!(ops.contains("5 Tr"));
}
#[test]
fn test_rendering_mode_fill_stroke_clip() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::FillStrokeClip);
let ops = context.generate_text_state_operations();
assert!(ops.contains("6 Tr"));
}
#[test]
fn test_rendering_mode_clip() {
let mut context = TextContext::new();
context.set_rendering_mode(TextRenderingMode::Clip);
let ops = context.generate_text_state_operations();
assert!(ops.contains("7 Tr"));
}
#[test]
fn test_text_state_parameters_chaining() {
let mut context = TextContext::new();
context
.set_character_spacing(1.5)
.set_word_spacing(2.0)
.set_horizontal_scaling(1.1)
.set_leading(14.0)
.set_text_rise(0.5)
.set_rendering_mode(TextRenderingMode::FillStroke);
let ops = context.generate_text_state_operations();
assert!(ops.contains("1.50 Tc"));
assert!(ops.contains("2.00 Tw"));
assert!(ops.contains("110.00 Tz"));
assert!(ops.contains("14.00 TL"));
assert!(ops.contains("0.50 Ts"));
assert!(ops.contains("2 Tr"));
}
#[test]
fn test_all_text_state_operators_generated() {
let mut context = TextContext::new();
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();
assert!(
ops.contains("Tc"),
"Character spacing operator (Tc) not found"
);
assert!(ops.contains("Tw"), "Word spacing operator (Tw) not found");
assert!(
ops.contains("Tz"),
"Horizontal scaling operator (Tz) not found"
);
assert!(ops.contains("TL"), "Leading operator (TL) not found");
assert!(ops.contains("Ts"), "Text rise operator (Ts) not found");
assert!(
ops.contains("Tr"),
"Text rendering mode operator (Tr) not found"
);
}
#[test]
fn test_text_color_operations() {
use crate::Color;
let mut context = TextContext::new();
context.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
context.apply_text_state_parameters();
let ops = context.operations();
assert!(
ops.contains("1.000 0.000 0.000 rg"),
"RGB fill color operator (rg) not found in: {ops}"
);
context.clear();
context.set_stroke_color(Color::rgb(0.0, 1.0, 0.0));
context.apply_text_state_parameters();
let ops = context.operations();
assert!(
ops.contains("0.000 1.000 0.000 RG"),
"RGB stroke color operator (RG) not found in: {ops}"
);
context.clear();
context.set_fill_color(Color::gray(0.5));
context.apply_text_state_parameters();
let ops = context.operations();
assert!(
ops.contains("0.500 g"),
"Gray fill color operator (g) not found in: {ops}"
);
context.clear();
context.set_stroke_color(Color::cmyk(0.2, 0.3, 0.4, 0.1));
context.apply_text_state_parameters();
let ops = context.operations();
assert!(
ops.contains("0.200 0.300 0.400 0.100 K"),
"CMYK stroke color operator (K) not found in: {ops}"
);
context.clear();
context.set_fill_color(Color::rgb(1.0, 0.0, 0.0));
context.set_stroke_color(Color::rgb(0.0, 0.0, 1.0));
context.apply_text_state_parameters();
let ops = context.operations();
assert!(
ops.contains("1.000 0.000 0.000 rg") && ops.contains("0.000 0.000 1.000 RG"),
"Both fill and stroke colors not found in: {ops}"
);
}
#[test]
fn test_used_characters_tracking_ascii() {
let mut context = TextContext::new();
context.write("Hello").unwrap();
let chars = context.get_used_characters();
assert!(chars.is_some());
let chars = chars.unwrap();
assert!(chars.contains(&'H'));
assert!(chars.contains(&'e'));
assert!(chars.contains(&'l'));
assert!(chars.contains(&'o'));
assert_eq!(chars.len(), 4); }
#[test]
fn test_used_characters_tracking_cjk() {
let mut context = TextContext::new();
context.set_font(Font::Custom("NotoSansCJK".to_string()), 12.0);
context.write("中文测试").unwrap();
let chars = context.get_used_characters();
assert!(chars.is_some());
let chars = chars.unwrap();
assert!(chars.contains(&'中'));
assert!(chars.contains(&'文'));
assert!(chars.contains(&'测'));
assert!(chars.contains(&'试'));
assert_eq!(chars.len(), 4);
}
#[test]
fn test_used_characters_empty_initially() {
let context = TextContext::new();
assert!(context.get_used_characters().is_none());
}
#[test]
fn test_used_characters_multiple_writes() {
let mut context = TextContext::new();
context.write("AB").unwrap();
context.write("CD").unwrap();
let chars = context.get_used_characters();
assert!(chars.is_some());
let chars = chars.unwrap();
assert!(chars.contains(&'A'));
assert!(chars.contains(&'B'));
assert!(chars.contains(&'C'));
assert!(chars.contains(&'D'));
assert_eq!(chars.len(), 4);
}
#[test]
fn nan_char_spacing_sanitised_at_emission() {
let mut ctx = TextContext::new();
ctx.set_character_spacing(f64::NAN);
ctx.write("hi").unwrap();
let ops = ctx.operations();
assert!(
ops.contains("0.00 Tc\n"),
"NaN char spacing must emit `0.00 Tc`, got: {ops:?}"
);
assert!(
!ops.contains("NaN") && !ops.contains("inf"),
"non-finite tokens must not appear in any Tc/Tw/Tz/TL/Ts emission, got: {ops:?}"
);
}
#[test]
fn pos_inf_word_spacing_sanitised_at_emission() {
let mut ctx = TextContext::new();
ctx.set_word_spacing(f64::INFINITY);
ctx.write("hi").unwrap();
let ops = ctx.operations();
assert!(
ops.contains("0.00 Tw\n"),
"+inf word spacing must emit `0.00 Tw`, got: {ops:?}"
);
assert!(
!ops.contains("inf"),
"`inf` must not appear in Tw output, got: {ops:?}"
);
}
#[test]
fn nan_horizontal_scaling_sanitised_at_emission() {
let mut ctx = TextContext::new();
ctx.set_horizontal_scaling(f64::NAN);
ctx.write("hi").unwrap();
let ops = ctx.operations();
assert!(
ops.contains("0.00 Tz\n"),
"NaN horizontal scaling must emit `0.00 Tz`, got: {ops:?}"
);
}
#[test]
fn nan_leading_and_text_rise_sanitised_at_emission() {
let mut ctx = TextContext::new();
ctx.set_leading(f64::NEG_INFINITY);
ctx.set_text_rise(f64::NAN);
ctx.write("hi").unwrap();
let ops = ctx.operations();
assert!(
ops.contains("0.00 TL\n"),
"-inf leading must emit `0.00 TL`, got: {ops:?}"
);
assert!(
ops.contains("0.00 Ts\n"),
"NaN text rise must emit `0.00 Ts`, got: {ops:?}"
);
}
#[test]
fn test_text_context_threads_metrics_store() {
use crate::text::metrics::{FontMetrics, FontMetricsStore};
let store = FontMetricsStore::new();
let ctx = TextContext::with_metrics_store(Some(store.clone()));
assert!(ctx.font_metrics_store_for_test().is_some());
store.register("X", FontMetrics::new(400));
assert_eq!(
ctx.font_metrics_store_for_test().unwrap().len(),
1,
"TextContext must hold a clone that shares the underlying registry"
);
}
}