use super::super::*;
#[test]
fn test_adjacent_table_cell_values_not_concatenated() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "$0.00".to_string(),
bbox: Rect::new(100.0, 700.0, 30.0, 10.0),
font_name: "F1".to_string(),
font_size: 10.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "$0.00".to_string(),
bbox: Rect::new(131.0, 700.0, 30.0, 10.0), font_name: "F1".to_string(),
font_size: 10.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(extractor.spans.len(), 1, "Adjacent spans should merge");
assert_eq!(
extractor.spans[0].text, "$0.00 $0.00",
"Adjacent table cell values should have space between them, got: '{}'",
extractor.spans[0].text
);
}
#[test]
fn test_adjacent_numeric_values_not_concatenated() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "100".to_string(),
bbox: Rect::new(200.0, 500.0, 18.0, 10.0),
font_name: "F1".to_string(),
font_size: 10.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "200".to_string(),
bbox: Rect::new(219.5, 500.0, 18.0, 10.0), font_name: "F1".to_string(),
font_size: 10.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(extractor.spans.len(), 1, "Adjacent spans should merge");
assert_eq!(
extractor.spans[0].text, "100 200",
"Adjacent numeric values should have space between them, got: '{}'",
extractor.spans[0].text
);
}
#[test]
fn test_word_fragments_zero_gap_no_space() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "Hel".to_string(),
bbox: Rect::new(100.0, 700.0, 18.0, 12.0),
font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "lo".to_string(),
bbox: Rect::new(118.0, 700.0, 12.0, 12.0), font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(extractor.spans.len(), 1, "Adjacent spans should merge");
assert_eq!(
extractor.spans[0].text, "Hello",
"Zero-gap word fragments should merge without space, got: '{}'",
extractor.spans[0].text
);
}
#[test]
fn test_merge_decimal_dollar_value_split_boxes() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "123456".to_string(),
bbox: Rect::new(382.3, 700.0, 39.6, 12.0),
font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "72".to_string(),
bbox: Rect::new(432.7, 700.0, 13.2, 12.0), font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
1,
"Decimal dollar value spans should merge into one"
);
assert_eq!(
extractor.spans[0].text, "123456.72",
"Integer and decimal parts should be joined with '.'"
);
}
#[test]
fn test_merge_decimal_value_small_integer_part() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "50".to_string(),
bbox: Rect::new(382.3, 700.0, 15.0, 12.0),
font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "00".to_string(),
bbox: Rect::new(407.0, 700.0, 13.2, 12.0), font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(extractor.spans.len(), 1);
assert_eq!(extractor.spans[0].text, "50.00");
}
#[test]
fn test_no_decimal_merge_for_non_digit_spans() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "Hello".to_string(),
bbox: Rect::new(382.3, 700.0, 39.6, 12.0),
font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "72".to_string(),
bbox: Rect::new(432.7, 700.0, 13.2, 12.0), font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
2,
"Non-digit spans should not be merged as decimal values"
);
}
#[test]
fn test_no_decimal_merge_for_long_decimal_part() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "123456".to_string(),
bbox: Rect::new(382.3, 700.0, 39.6, 12.0),
font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "723".to_string(),
bbox: Rect::new(432.7, 700.0, 18.0, 12.0), font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
2,
"3-digit decimal part should not trigger decimal merge"
);
}
fn digit_test_span(text: &str, bbox: Rect, font_size: f32) -> TextSpan {
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: text.to_string(),
bbox,
font_name: "F1".to_string(),
font_size,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
}
}
#[test]
fn test_no_decimal_merge_with_intervening_comma_glyph() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
digit_test_span("1", Rect::new(100.0, 700.0, 3.5, 7.0), 7.0),
digit_test_span("0", Rect::new(110.0, 700.0, 3.5, 7.0), 7.0),
digit_test_span(",", Rect::new(104.8, 699.0, 1.8, 3.0), 7.0),
];
extractor.merge_adjacent_spans();
assert!(
!extractor.spans.iter().any(|s| s.text.contains("1.0")),
"digits separated by a drawn comma must not merge into a decimal, got {:?}",
extractor.spans.iter().map(|s| s.text.clone()).collect::<Vec<_>>()
);
}
#[test]
fn test_no_decimal_merge_across_font_sizes() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
digit_test_span("4", Rect::new(200.0, 700.0, 4.0, 7.0), 7.0),
digit_test_span("3", Rect::new(212.0, 700.0, 6.5, 12.0), 12.0),
];
extractor.merge_adjacent_spans();
assert!(
!extractor.spans.iter().any(|s| s.text.contains('.')),
"digit runs at mismatched font sizes must not merge into a decimal, got {:?}",
extractor.spans.iter().map(|s| s.text.clone()).collect::<Vec<_>>()
);
}
#[test]
fn test_decimal_merge_with_ink_elsewhere_on_line_still_joins() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
digit_test_span("123456", Rect::new(382.3, 700.0, 39.6, 12.0), 12.0),
digit_test_span("72", Rect::new(432.7, 700.0, 13.2, 12.0), 12.0), digit_test_span(",", Rect::new(300.0, 700.0, 2.5, 4.0), 12.0),
];
extractor.merge_adjacent_spans();
assert!(
extractor.spans.iter().any(|s| s.text == "123456.72"),
"split-box amount must still merge when the line's other ink is outside the gap, got {:?}",
extractor.spans.iter().map(|s| s.text.clone()).collect::<Vec<_>>()
);
}
#[test]
fn test_no_decimal_merge_for_wide_subscript_digits() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "1".to_string(),
bbox: Rect::new(100.0, 700.0, 3.5, 7.0),
font_name: "F1".to_string(),
font_size: 7.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "0".to_string(),
bbox: Rect::new(114.5, 700.0, 3.5, 7.0), font_name: "F1".to_string(),
font_size: 7.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
2,
"Widely-spaced subscript digits should not merge into a decimal value"
);
assert!(
!extractor.spans.iter().any(|s| s.text.contains('.')),
"No invented decimal point should appear between subscript digits"
);
}
#[test]
fn test_decimal_merge_just_under_ceiling_still_joins() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::legacy();
extractor.spans = vec![
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "1234".to_string(),
bbox: Rect::new(200.0, 700.0, 24.0, 12.0),
font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 0,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
TextSpan {
provenance: None,
text_rise: 0.0,
artifact_type: None,
text: "56".to_string(),
bbox: Rect::new(238.4, 700.0, 12.0, 12.0), font_name: "F1".to_string(),
font_size: 12.0,
font_weight: FontWeight::Normal,
color: Color::black(),
mcid: None,
mcid_scope: None,
sequence: 1,
split_boundary_before: false,
offset_semantic: false,
is_italic: false,
is_monospace: false,
char_spacing: 0.0,
word_spacing: 0.0,
horizontal_scaling: 100.0,
primary_detected: false,
char_widths: vec![],
char_x_offsets: Vec::new(),
heading_level: None,
rotation_degrees: 0.0,
wmode: 0,
rtl_draw_logical: false,
mirrored: false,
page_rotation_applied: 0,
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
1,
"Amount just under the ceiling should still merge"
);
assert_eq!(extractor.spans[0].text, "1234.56");
}
#[test]
fn test_cross_font_word_glue_single_letter_prefix() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::default();
extractor.spans = vec![
TextSpan {
text: "S".to_string(),
bbox: Rect::new(72.0, 700.0, 10.0, 12.0),
font_name: "Helvetica-Bold".to_string(),
font_weight: FontWeight::Bold,
font_size: 12.0,
..TextSpan::default()
},
TextSpan {
text: "ales".to_string(),
bbox: Rect::new(82.0, 700.0, 30.0, 12.0),
font_name: "Helvetica".to_string(),
font_weight: FontWeight::Normal,
font_size: 12.0,
..TextSpan::default()
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
1,
"cross_font_word_glue should merge 'S' + 'ales' into 'Sales'"
);
assert_eq!(extractor.spans[0].text, "Sales");
assert_eq!(extractor.spans[0].font_weight, FontWeight::Normal);
}
#[test]
fn test_fold_offset_into_buffer_matches_spec_displacement() {
let mut extractor = TextExtractor::new();
{
let st = extractor.state_stack.current_mut();
st.font_size = 10.0;
st.horizontal_scaling = 100.0; }
let mut buffer = TjBuffer::new(extractor.state_stack.current(), None, None);
buffer.char_widths.push(5.0);
buffer.accumulated_width = 5.0;
extractor.fold_offset_into_buffer(&mut buffer, -120.0);
let expected = 1.2_f32;
assert!((buffer.char_widths.last().unwrap() - (5.0 + expected)).abs() < 1e-4);
assert!((buffer.accumulated_width - (5.0 + expected)).abs() < 1e-4);
let sum: f32 = buffer.char_widths.iter().sum();
assert!((sum - buffer.accumulated_width).abs() < 1e-4);
let mut empty = TjBuffer::new(extractor.state_stack.current(), None, None);
extractor.fold_offset_into_buffer(&mut empty, -120.0);
assert!(empty.char_widths.is_empty());
assert_eq!(empty.accumulated_width, 0.0);
}
#[test]
fn test_cross_font_word_variable_not_glued() {
let mut extractor = TextExtractor::new();
extractor.merging_config = SpanMergingConfig::default();
extractor.spans = vec![
TextSpan {
text: "solution".to_string(),
bbox: Rect::new(72.0, 700.0, 40.0, 10.0),
font_name: "NimbusRomNo9L-Regu".to_string(),
font_size: 10.0,
..TextSpan::default()
},
TextSpan {
text: "U".to_string(),
bbox: Rect::new(114.4, 700.0, 7.0, 10.0),
font_name: "NimbusRomNo9L-Ital".to_string(),
font_size: 10.0,
..TextSpan::default()
},
];
extractor.merge_adjacent_spans();
assert_eq!(
extractor.spans.len(),
2,
"a 0.24em word-space gap across a font change must NOT glue (drop-cap glue is for ~0 gaps)"
);
}