implement Subscript/Superscript detection

This commit is contained in:
Abimael Martell
2026-02-07 22:27:54 -08:00
parent 4f66cf1a6e
commit d530f9c888
2 changed files with 88 additions and 8 deletions
+21 -2
View File
@@ -43,13 +43,32 @@ impl TextLine {
if i == 0 { if i == 0 {
result.push_str(text); result.push_str(text);
} else { } else {
let prev_item = &self.items[i - 1];
// Don't add space before/after hyphens for hyphenated words // Don't add space before/after hyphens for hyphenated words
let prev_ends_with_hyphen = result.ends_with('-'); let prev_ends_with_hyphen = result.ends_with('-');
let curr_is_hyphen = text.trim() == "-"; let curr_is_hyphen = text.trim() == "-";
let curr_starts_with_hyphen = text.starts_with('-'); let curr_starts_with_hyphen = text.starts_with('-');
if prev_ends_with_hyphen || curr_is_hyphen || curr_starts_with_hyphen { // Detect subscript/superscript: smaller font size and/or Y offset
// No space for hyphenated words // Subscripts/superscripts are typically 60-80% of normal font size
// and have a vertical offset of 1-3 points
let font_ratio = item.font_size / prev_item.font_size;
let reverse_font_ratio = prev_item.font_size / item.font_size;
let y_diff = (item.y - prev_item.y).abs();
// Current item is subscript/superscript (smaller than previous)
let is_sub_super = font_ratio < 0.85 && y_diff > 1.0;
// Previous item was subscript/superscript (returning to normal size)
let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0;
if prev_ends_with_hyphen
|| curr_is_hyphen
|| curr_starts_with_hyphen
|| is_sub_super
|| was_sub_super
{
// No space for hyphenated words or subscript/superscript
result.push_str(text); result.push_str(text);
} else { } else {
result.push(' '); result.push(' ');
+67 -6
View File
@@ -123,8 +123,9 @@ fn detect_table_in_region(items: &[(usize, &TextItem)]) -> Option<Table> {
return None; return None;
} }
// Build the table grid // Build the table grid - first collect items per cell, then join properly
let mut cells: Vec<Vec<String>> = vec![vec![String::new(); columns.len()]; rows.len()]; let mut cell_items: Vec<Vec<Vec<&TextItem>>> =
vec![vec![Vec::new(); columns.len()]; rows.len()];
let mut item_indices = Vec::new(); let mut item_indices = Vec::new();
for (idx, item) in items { for (idx, item) in items {
@@ -132,14 +133,26 @@ fn detect_table_in_region(items: &[(usize, &TextItem)]) -> Option<Table> {
let row = find_row_index(&rows, item.y); let row = find_row_index(&rows, item.y);
if let (Some(col), Some(row)) = (col, row) { if let (Some(col), Some(row)) = (col, row) {
if !cells[row][col].is_empty() { cell_items[row][col].push(item);
cells[row][col].push(' ');
}
cells[row][col].push_str(item.text.trim());
item_indices.push(*idx); item_indices.push(*idx);
} }
} }
// Sort items within each cell by X position and join with subscript-aware spacing
let mut cells: Vec<Vec<String>> = Vec::with_capacity(rows.len());
for row_items in &mut cell_items {
let mut row_cells = Vec::with_capacity(columns.len());
for col_items in row_items.iter_mut() {
// Sort by X position
col_items.sort_by(|a, b| a.x.partial_cmp(&b.x).unwrap_or(std::cmp::Ordering::Equal));
// Join items with subscript-aware spacing
let text = join_cell_items(col_items);
row_cells.push(text);
}
cells.push(row_cells);
}
// Validation 1: most rows should have content in first column // Validation 1: most rows should have content in first column
let rows_with_first_col = cells.iter().filter(|row| !row[0].is_empty()).count(); let rows_with_first_col = cells.iter().filter(|row| !row[0].is_empty()).count();
if rows_with_first_col < rows.len() / 2 { if rows_with_first_col < rows.len() / 2 {
@@ -300,6 +313,54 @@ fn find_row_index(rows: &[f32], y: f32) -> Option<usize> {
.map(|(idx, _)| idx) .map(|(idx, _)| idx)
} }
/// Join cell items with subscript/superscript-aware spacing
/// Same logic as TextLine::text() but for table cells
fn join_cell_items(items: &[&TextItem]) -> String {
let mut result = String::new();
for (i, item) in items.iter().enumerate() {
let text = item.text.trim();
if text.is_empty() {
continue;
}
if result.is_empty() {
result.push_str(text);
} else {
let prev_item = items[i - 1];
// Don't add space before/after hyphens
let prev_ends_with_hyphen = result.ends_with('-');
let curr_is_hyphen = text == "-";
let curr_starts_with_hyphen = text.starts_with('-');
// Detect subscript/superscript: smaller font size and/or Y offset
let font_ratio = item.font_size / prev_item.font_size;
let reverse_font_ratio = prev_item.font_size / item.font_size;
let y_diff = (item.y - prev_item.y).abs();
// Current item is subscript/superscript (smaller than previous)
let is_sub_super = font_ratio < 0.85 && y_diff > 1.0;
// Previous item was subscript/superscript (returning to normal size)
let was_sub_super = reverse_font_ratio < 0.85 && y_diff > 1.0;
if prev_ends_with_hyphen
|| curr_is_hyphen
|| curr_starts_with_hyphen
|| is_sub_super
|| was_sub_super
{
result.push_str(text);
} else {
result.push(' ');
result.push_str(text);
}
}
}
result
}
/// Format a table as markdown /// Format a table as markdown
pub fn table_to_markdown(table: &Table) -> String { pub fn table_to_markdown(table: &Table) -> String {
if table.cells.is_empty() || table.cells[0].is_empty() { if table.cells.is_empty() || table.cells[0].is_empty() {