use super::super::*;
use crate::fonts::{Encoding, LazyCMap};
#[test]
fn test_bimodal_gap_split_heading() {
let gaps = [-0.5, -0.7, -0.3, 3.72, 3.70, 3.68, -0.4, 3.71];
let split = TextExtractor::bimodal_gap_split(&gaps, 20.5);
let split = split.expect("clearly bimodal line must yield a split");
assert!(
split > 0.0 && split < 3.5,
"split {split} must separate the ~0 and ~3.7pt clusters"
);
}
#[test]
fn test_bimodal_gap_split_uniform_word_spacing_none() {
let gaps = [6.0, 6.1, 5.9, 6.05, 5.95];
assert!(TextExtractor::bimodal_gap_split(&gaps, 12.0).is_none());
}
#[test]
fn test_bimodal_gap_split_single_word_none() {
let gaps = [-0.5, -0.7, -0.3, 0.1, -0.4];
assert!(TextExtractor::bimodal_gap_split(&gaps, 20.5).is_none());
}
#[test]
fn test_bimodal_gap_split_multilevel_footer() {
let gaps = [-0.1, -0.2, -0.15, 1.14, -0.1, -0.05, 2.75, -0.2];
let split = TextExtractor::bimodal_gap_split(&gaps, 11.0).expect("a multi-level line must yield a split");
assert!(
split > 0.0 && split < 1.14,
"split {split} must sit below the narrow 1.14pt word gap so both it and the wide space split"
);
}
#[test]
fn test_gap_has_intervening_glyph() {
let r = |x, y, w, h| crate::geometry::Rect {
x,
y,
width: w,
height: h,
};
let left = r(0.0, 0.0, 10.0, 10.0);
let right = r(24.0, 0.0, 10.0, 10.0);
let subscript = r(13.0, -3.0, 8.0, 8.0);
assert!(
gap_has_intervening_glyph(&[left, right, subscript], &left, &right),
"a full subscript occupying the gap must be detected"
);
let descender_edge = r(9.0, -4.0, 3.0, 6.0);
assert!(
!gap_has_intervening_glyph(&[left, right, descender_edge], &left, &right),
"a descender edge clipping the gap must not be treated as an intervening glyph"
);
}
#[test]
fn test_advances_along_writing_axis_by_quadrant() {
let m = |a, b, c, d| Matrix {
a,
b,
c,
d,
e: 100.0,
f: 500.0,
};
let fs = 10.0;
let at =
|mat: Matrix, de: f32, df: f32| TextExtractor::advances_along_writing_axis(mat, 0, mat.e + de, mat.f + df, fs);
let upright = m(1.0, 0.0, 0.0, 1.0);
assert!(at(upright, 14.0, 0.0), "upright advance must continue");
assert!(!at(upright, 0.0, -14.0), "upright line break must not");
assert!(!at(upright, -14.0, 0.0), "upright backwards must not");
assert!(at(upright, 14.0, 4.0), "upright sub-glyph offset must not be vetoed");
assert!(!at(upright, 14.0, 8.0), "upright line step must be vetoed");
let cw = m(0.0, 1.0, -1.0, 0.0);
assert!(at(cw, 0.0, 14.0), "90° along-axis advance must not be vetoed");
assert!(!at(cw, 14.0, 0.0), "90° line break must not continue");
assert!(at(cw, -4.0, 14.0), "90° sub-glyph offset must not be vetoed");
assert!(!at(cw, -8.0, 14.0), "90° line step must be vetoed");
let ccw = m(0.0, -1.0, 1.0, 0.0);
assert!(at(ccw, 0.0, -14.0), "270° along-axis advance must not be vetoed");
assert!(!at(ccw, 0.0, 14.0), "270° backwards advance must not continue");
assert!(!at(ccw, 14.0, 0.0), "270° line break must not continue");
let flip = m(-1.0, 0.0, 0.0, -1.0);
assert!(at(flip, -14.0, 0.0), "180° along-axis advance must not be vetoed");
assert!(!at(flip, 14.0, 0.0), "180° backwards advance must not continue");
let degenerate = m(0.0, 0.0, 0.0, 0.0);
assert!(at(degenerate, 14.0, 0.0));
assert!(!at(degenerate, -14.0, 0.0));
for (de, df) in [(14.0, 0.0), (0.0, 14.0), (-14.0, 0.0), (0.0, -14.0)] {
assert!(
TextExtractor::advances_along_writing_axis(cw, 1, cw.e + de, cw.f + df, fs),
"vertical run vetoed at ({de}, {df})"
);
}
}
#[test]
fn test_snap_run_rotation() {
let m = |a, b, c, d| Matrix {
a,
b,
c,
d,
e: 0.0,
f: 0.0,
};
assert_eq!(snap_run_rotation(&m(12.0, 0.0, 0.0, 12.0)), 0.0);
assert_eq!(snap_run_rotation(&m(12.0, 1e-5, -1e-5, 12.0)), 0.0);
assert_eq!(snap_run_rotation(&m(0.0, 12.0, -12.0, 0.0)), 90.0);
assert_eq!(snap_run_rotation(&m(0.0, -12.0, 12.0, 0.0)), -90.0);
assert_eq!(snap_run_rotation(&m(-12.0, 0.0, 0.0, -12.0)), 180.0);
assert_eq!(snap_run_rotation(&m(-12.0, 1e-5, -1e-5, -12.0)), 180.0);
let r = 12.0_f32;
let th = 88.0_f32.to_radians();
assert_eq!(
snap_run_rotation(&m(r * th.cos(), r * th.sin(), -r * th.sin(), r * th.cos())),
90.0
);
let th = 45.0_f32.to_radians();
let got = snap_run_rotation(&m(r * th.cos(), r * th.sin(), -r * th.sin(), r * th.cos()));
assert!((got - 45.0).abs() < 0.5, "45° should pass through, got {got}");
}
pub(super) fn create_test_font() -> FontInfo {
FontInfo {
base_font: "Times-Roman".to_string(),
subtype: "Type1".to_string(),
encoding: Encoding::Standard("WinAnsiEncoding".to_string()),
to_unicode: None,
font_weight: None,
flags: None,
stem_v: None,
ascent: 0.95,
descent: -0.35,
embedded_font_data: None,
truetype_cmap: std::sync::OnceLock::new(),
embedded_glyph_names: std::sync::OnceLock::new(),
is_truetype_font: false,
widths: None,
first_char: None,
last_char: None,
font_matrix_a: 0.001,
default_width: 1000.0,
cid_to_gid_map: None,
cid_system_info: None,
cid_font_type: None,
cid_widths: None,
cid_default_width: 1000.0,
has_explicit_dw: false,
cff_gid_map: None,
multi_char_map: HashMap::new(),
byte_to_char_table: std::sync::OnceLock::new(),
type0_unicode_memo: std::sync::Arc::new(std::sync::Mutex::new(std::collections::HashMap::new())),
byte_to_width_table: std::sync::OnceLock::new(),
weight_memo: std::sync::OnceLock::new(),
italic_memo: std::sync::OnceLock::new(),
std14_memo: std::sync::OnceLock::new(),
diff_glyph_names: std::collections::HashMap::new(),
wmode: 0,
cid_vertical_metrics: None,
cid_default_vertical_metrics: crate::fonts::VerticalMetrics::SPEC_DEFAULT,
cjk_substitution: None,
embedded_cid_map: None,
}
}
#[test]
fn test_text_extractor_new() {
let extractor = TextExtractor::new();
assert_eq!(extractor.char_count(), 0);
}
#[test]
fn test_text_extractor_add_font() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
assert_eq!(extractor.fonts.len(), 1);
}
#[test]
fn test_extract_simple_text() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf 100 700 Td (Hello) Tj ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 5);
assert_eq!(chars[0].char, 'H');
assert_eq!(chars[1].char, 'e');
assert_eq!(chars[2].char, 'l');
assert_eq!(chars[3].char, 'l');
assert_eq!(chars[4].char, 'o');
}
#[test]
fn test_extract_with_matrix() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf 1 0 0 1 100 700 Tm (Hi) Tj ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 2);
assert_eq!(chars[0].char, 'H');
assert_eq!(chars[1].char, 'i');
assert!(chars[0].bbox.x >= 99.0 && chars[0].bbox.x <= 101.0);
}
#[test]
fn test_ctm_applied_to_text_position() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"q 1 0 0 1 100 200 cm BT /F1 12 Tf (A) Tj ET Q";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 1);
assert_eq!(chars[0].char, 'A');
assert!(
chars[0].bbox.x >= 99.0 && chars[0].bbox.x <= 101.0,
"X position should be ~100 (got {})",
chars[0].bbox.x
);
assert!(
chars[0].bbox.y >= 199.0 && chars[0].bbox.y <= 201.0,
"Y position should be ~200 (got {})",
chars[0].bbox.y
);
}
#[test]
fn test_char_mode_rotated_ctm_survives_large_hostile_stream() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let mut cs = Vec::new();
cs.extend_from_slice(b"q\n0 1 -1 0 612 0 cm\n");
cs.extend_from_slice(&[b'('; 1500]);
cs.push(b'\n');
for i in 0..13000u32 {
let line = format!(
"{}.0 {}.0 m {}.0 {}.0 l S\n",
i % 500,
(i * 7) % 500,
(i * 3) % 500,
(i * 11) % 500
);
cs.extend_from_slice(line.as_bytes());
}
assert!(cs.len() > 256 * 1024, "stream must exceed the 256KB prescan threshold");
cs.extend_from_slice(b"BT /F1 12 Tf 100 200 Td (Hello) Tj ET\nQ\n");
let chars = extractor.extract(&cs).unwrap();
let mut glyphs: Vec<char> = chars.iter().map(|c| c.char).collect();
glyphs.sort_unstable();
assert_eq!(glyphs, vec!['H', 'e', 'l', 'l', 'o']);
for c in &chars {
assert!(
(c.rotation_degrees - 90.0).abs() < 1.0,
"expected 90 degrees from the cm before BT, got {}",
c.rotation_degrees
);
}
}
#[test]
fn test_ctm_scaling_applied_to_text_position() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"q 2 0 0 2 0 0 cm BT /F1 12 Tf 1 0 0 1 50 100 Tm (B) Tj ET Q";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 1);
assert_eq!(chars[0].char, 'B');
assert!(
chars[0].bbox.x >= 99.0 && chars[0].bbox.x <= 101.0,
"X position should be ~100 (got {})",
chars[0].bbox.x
);
assert!(
chars[0].bbox.y >= 199.0 && chars[0].bbox.y <= 201.0,
"Y position should be ~200 (got {})",
chars[0].bbox.y
);
}
#[test]
fn test_ctm_combined_with_text_matrix() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"q 1 0 0 1 50 50 cm BT /F1 12 Tf 1 0 0 1 25 25 Tm (C) Tj ET Q";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 1);
assert_eq!(chars[0].char, 'C');
assert!(
chars[0].bbox.x >= 74.0 && chars[0].bbox.x <= 76.0,
"X position should be ~75 (got {})",
chars[0].bbox.x
);
assert!(
chars[0].bbox.y >= 74.0 && chars[0].bbox.y <= 76.0,
"Y position should be ~75 (got {})",
chars[0].bbox.y
);
}
#[test]
fn test_extract_with_tj_array() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf 0 0 Td [(H)(i)] TJ ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 2);
assert_eq!(chars[0].char, 'H');
assert_eq!(chars[1].char, 'i');
}
#[test]
fn test_extract_type0_multibyte_character_extraction() {
let mut extractor = TextExtractor::new();
let mut font = create_test_font();
font.subtype = "Type0".to_string();
font.encoding = Encoding::Standard("Identity-H".to_string());
let cmap_data = b"
/CIDInit /ProcSet findresource begin
12 dict begin
begincmap
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def
/CMapName /Adobe-Identity-UCS def
/CMapType 2 def
1 begincodespacerange <0000> <FFFF> endcodespacerange
2 beginbfchar
<4E2D> <4E2D>
<6587> <6587>
endbfchar
endcmap
CMapName currentdict /CMap defineresource pop
end
end
";
let lazy_cmap = LazyCMap::new(cmap_data.to_vec());
font.to_unicode = Some(lazy_cmap);
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf 0 0 Td <4E2D6587> Tj ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 2);
assert_eq!(chars[0].char, '中');
assert_eq!(chars[1].char, '文');
}
#[test]
fn test_extract_color() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT 1 0 0 rg /F1 12 Tf 0 0 Td (R) Tj ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 1);
assert_eq!(chars[0].char, 'R');
assert_eq!(chars[0].color.r, 1.0);
assert_eq!(chars[0].color.g, 0.0);
assert_eq!(chars[0].color.b, 0.0);
}
#[test]
fn test_fill_color_scn_before_bt_after_bdc_not_dropped() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"/Shape <</MCID 3 >>BDC \
0.2 0.2 0.604 scn \
BT /F1 12 Tf 100 700 Td (Blue Heading) Tj ET \
EMC";
let spans = extractor.extract_text_spans(stream).unwrap();
assert_eq!(spans.len(), 1);
assert!(
(spans[0].color.r - 0.2).abs() < 0.01,
"expected blue fill (0.2, 0.2, 0.604), got {:?}",
spans[0].color
);
assert!((spans[0].color.g - 0.2).abs() < 0.01);
assert!((spans[0].color.b - 0.604).abs() < 0.01);
}
#[test]
fn test_fill_color_scn_after_q_before_bt_not_dropped() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"q 1 0 0 1 0 0 cm Q \
1 1 0 scn \
BT /F1 12 Tf 100 700 Td (Gold Author) Tj ET";
let spans = extractor.extract_text_spans(stream).unwrap();
assert_eq!(spans.len(), 1);
assert!(
(spans[0].color.r - 1.0).abs() < 0.01,
"expected gold fill (1, 1, 0), got {:?}",
spans[0].color
);
assert!((spans[0].color.g - 1.0).abs() < 0.01);
assert!((spans[0].color.b - 0.0).abs() < 0.01);
}
#[test]
fn test_fill_color_scn_inside_open_text_object_still_works() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf 100 700 Td (Black Text) Tj \
0.2 0.2 0.604 scn \
0 -20 Td (Blue Text) Tj ET";
let spans = extractor.extract_text_spans(stream).unwrap();
assert_eq!(spans.len(), 2);
assert!(
(spans[0].color.r - 0.0).abs() < 0.01 && (spans[0].color.b - 0.0).abs() < 0.01,
"first run should still be default black, got {:?}",
spans[0].color
);
assert!(
(spans[1].color.r - 0.2).abs() < 0.01,
"second run should be blue (0.2, 0.2, 0.604), got {:?}",
spans[1].color
);
assert!((spans[1].color.g - 0.2).abs() < 0.01);
assert!((spans[1].color.b - 0.604).abs() < 0.01);
}
#[test]
fn test_is_monospace_from_font_flags() {
let mut mono_font = create_test_font();
mono_font.base_font = "Courier".to_string();
mono_font.flags = Some(1);
let mut extractor = TextExtractor::new();
extractor.add_font("F1".to_string(), mono_font);
let stream = b"BT /F1 12 Tf 100 700 Td (Code) Tj ET";
let spans = extractor.extract_text_spans(stream).unwrap();
assert!(!spans.is_empty(), "should produce at least one span");
assert!(
spans[0].is_monospace,
"Courier with FixedPitch flag should be monospace, got is_monospace=false"
);
let mut prop_font = create_test_font();
prop_font.base_font = "Helvetica".to_string();
prop_font.flags = Some(0);
let mut extractor2 = TextExtractor::new();
extractor2.add_font("F2".to_string(), prop_font);
let stream2 = b"BT /F2 12 Tf 100 700 Td (Text) Tj ET";
let spans2 = extractor2.extract_text_spans(stream2).unwrap();
assert!(!spans2.is_empty(), "should produce at least one span");
assert!(
!spans2[0].is_monospace,
"Helvetica without FixedPitch flag should not be monospace"
);
let mut mono_name_font = create_test_font();
mono_name_font.base_font = "DejaVuSansMono".to_string();
mono_name_font.flags = None;
let mut extractor3 = TextExtractor::new();
extractor3.add_font("F3".to_string(), mono_name_font);
let stream3 = b"BT /F3 12 Tf 100 700 Td (Mono) Tj ET";
let spans3 = extractor3.extract_text_spans(stream3).unwrap();
assert!(!spans3.is_empty(), "should produce at least one span");
assert!(
spans3[0].is_monospace,
"Font named DejaVuSansMono should be detected as monospace via name heuristic"
);
}
#[test]
fn test_prescan_marked_content_scope_does_not_leak_across_regions() {
let mut cs = Vec::new();
cs.extend_from_slice(b"/PlacedPDF /MC0 BDC\n");
cs.extend_from_slice(b"BT /F1 12 Tf 100 700 Td (Figure Label) Tj ET\n");
for i in 0..13000u32 {
let line = format!(
"{}.0 {}.0 m {}.0 {}.0 l n\n",
i % 500,
(i * 7) % 500,
(i * 3) % 500,
(i * 11) % 500
);
cs.extend_from_slice(line.as_bytes());
}
cs.extend_from_slice(b"EMC\n");
cs.extend_from_slice(b"BT /F1 12 Tf 100 600 Td (Body Text After Figure) Tj ET\n");
assert!(cs.len() > 256 * 1024, "stream must exceed 256KB prescan threshold");
let font = create_test_font();
let mut extractor = TextExtractor::new();
extractor.add_font("F1".to_string(), font);
let spans = extractor.extract_text_spans(&cs).unwrap();
let all_text: String = spans.iter().map(|s| s.text.as_str()).collect::<Vec<_>>().join(" ");
assert!(
!all_text.contains("Figure Label"),
"text inside the /PlacedPDF scope must still be suppressed, got: {all_text:?}"
);
assert!(
all_text.contains("Body Text After Figure"),
"text after the /PlacedPDF scope closes (EMC) must not be suppressed \
just because the EMC fell outside the prescanned region, got: {all_text:?}"
);
}
#[test]
fn test_invisible_text_is_never_monospace() {
let mut ocr_font = create_test_font();
ocr_font.base_font = "GlyphLessFont".to_string();
ocr_font.flags = Some(1);
let mut extractor = TextExtractor::new();
extractor.add_font("F1".to_string(), ocr_font);
let stream = b"BT /F1 12 Tf 3 Tr 100 700 Td (\"I don't know,\" she said.) Tj ET";
let spans = extractor.extract_text_spans(stream).unwrap();
assert!(!spans.is_empty(), "should produce at least one span");
assert!(
!spans[0].is_monospace,
"invisible (Tr 3) text under a FixedPitch OCR font must not be \
classified monospace"
);
let mut visible_font = create_test_font();
visible_font.base_font = "Courier".to_string();
visible_font.flags = Some(1);
let mut extractor2 = TextExtractor::new();
extractor2.add_font("F2".to_string(), visible_font);
let stream2 = b"BT /F2 12 Tf 100 700 Td (let x = 1;) Tj ET";
let spans2 = extractor2.extract_text_spans(stream2).unwrap();
assert!(!spans2.is_empty(), "should produce at least one span");
assert!(
spans2[0].is_monospace,
"visible FixedPitch text must still be classified monospace"
);
}
#[test]
fn test_extract_save_restore() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf q /F1 14 Tf (A) Tj Q (B) Tj ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 2);
assert_eq!(chars[0].font_size, 14.0);
assert_eq!(chars[1].font_size, 12.0);
}
#[test]
fn test_extract_no_font() {
let mut extractor = TextExtractor::new();
let stream = b"BT /F1 12 Tf (ABC) Tj ET";
let chars = extractor.extract(stream).unwrap();
assert_eq!(chars.len(), 3);
}
#[test]
fn test_char_count() {
let mut extractor = TextExtractor::new();
assert_eq!(extractor.char_count(), 0);
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf (Test) Tj ET";
extractor.extract(stream).unwrap();
assert_eq!(extractor.char_count(), 4);
}
#[test]
fn test_clear() {
let mut extractor = TextExtractor::new();
let font = create_test_font();
extractor.add_font("F1".to_string(), font);
let stream = b"BT /F1 12 Tf (Test) Tj ET";
extractor.extract(stream).unwrap();
assert_eq!(extractor.char_count(), 4);
extractor.clear();
assert_eq!(extractor.char_count(), 0);
}
#[test]
fn test_default() {
let extractor = TextExtractor::default();
assert_eq!(extractor.char_count(), 0);
}
#[test]
fn test_space_decision_boundary_space() {
let config = SpanMergingConfig::default();
let fonts = std::collections::HashMap::new();
let decision = should_insert_space(
"word ", "next", 0.0, 12.0, "TestFont", &fonts, false, &config, None, None, 12.0, 12.0,
);
assert!(!decision.insert_space);
assert_eq!(decision.source, SpaceSource::AlreadyPresent);
let decision = should_insert_space(
"word", " next", 0.0, 12.0, "TestFont", &fonts, false, &config, None, None, 12.0, 12.0,
);
assert!(!decision.insert_space);
assert_eq!(decision.source, SpaceSource::AlreadyPresent);
}
#[test]
fn test_space_decision_digit_digit_gap_threshold() {
let config = SpanMergingConfig::default();
let fonts = std::collections::HashMap::new();
let kerning = should_insert_space(
"123", "456", 0.3, 12.0, "TestFont", &fonts, false, &config, None, None, 12.0, 12.0,
);
assert!(
!kerning.insert_space,
"Kerning-sized gap (0.3pt) between digits must not split the number, got: {:?}",
kerning
);
let table_cells = should_insert_space(
"123", "456", 2.0, 12.0, "TestFont", &fonts, false, &config, None, None, 12.0, 12.0,
);
assert!(
table_cells.insert_space,
"2pt gap between digits should still split adjacent table values, got: {:?}",
table_cells
);
}
#[test]
fn test_split_boundary_merges_with_space() {
let spans = vec![
TextSpan {
text: "the".to_string(),
bbox: Rect {
x: 0.0,
y: 100.0,
width: 10.0,
height: 12.0,
},
font_name: "Arial".to_string(),
sequence: 0,
split_boundary_before: false,
..TextSpan::default()
},
TextSpan {
text: "General".to_string(),
bbox: Rect {
x: 10.0,
y: 100.0,
width: 25.0,
height: 12.0,
},
font_name: "Arial".to_string(),
sequence: 1,
split_boundary_before: true,
..TextSpan::default()
},
];
let mut extractor = TextExtractor::new();
extractor.spans = spans;
extractor.merging_config = SpanMergingConfig::default();
extractor.merge_adjacent_spans();
assert_eq!(extractor.spans.len(), 1);
assert_eq!(extractor.spans[0].text, "the General");
}
#[test]
fn test_has_boundary_space() {
assert!(has_boundary_space("word ", "next"));
assert!(has_boundary_space("word", " next"));
assert!(has_boundary_space("word ", " next"));
assert!(!has_boundary_space("word", "next"));
assert!(has_boundary_space("word\t", "next"));
assert!(has_boundary_space("word\n", "next"));
assert!(has_boundary_space("word", "\tnext"));
}
#[test]
fn test_text_extraction_config_new_defaults() {
let config = TextExtractionConfig::new();
assert_eq!(config.space_insertion_threshold, -120.0);
assert_eq!(config.word_margin_ratio, 0.1);
assert!(!config.use_adaptive_tj_threshold);
assert!(config.profile.is_none());
}
#[test]
fn test_text_extraction_config_with_space_threshold() {
let config = TextExtractionConfig::with_space_threshold(-80.0);
assert_eq!(config.space_insertion_threshold, -80.0);
assert_eq!(config.word_margin_ratio, 0.1);
assert!(!config.use_adaptive_tj_threshold);
}
#[test]
fn test_text_extraction_config_with_word_margin_ratio() {
let config = TextExtractionConfig::with_word_margin_ratio(0.15);
assert_eq!(config.word_margin_ratio, 0.15);
assert!(config.use_adaptive_tj_threshold);
assert_eq!(config.space_insertion_threshold, -120.0); }