Improve text extraction with visual reading order and header detection

Major changes:
- Switch from lopdf.extract_text() to position-aware extraction
- Text is now sorted by visual reading order (top→bottom, left→right)
- Fixed font size calculation to account for text matrix scaling
- Headers are now detected based on font size ratios
- Skip very short text (≤3 chars) for header detection to avoid drop caps

This significantly improves output quality for PDFs with complex layouts.
Before: Title appeared at end, no structure detected
After: Correct reading order, headers marked with # syntax

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-07 13:34:37 -08:00
co-authored by Claude Opus 4.5
parent f99d155c52
commit c14e26495d
6 changed files with 107 additions and 27 deletions
+22 -6
View File
@@ -175,14 +175,15 @@ fn extract_page_text_items(
extract_text_from_operand(&op.operands[0], doc, &fonts, &current_font)
{
if !text.trim().is_empty() {
let rendered_size = effective_font_size(current_font_size, &text_matrix);
items.push(TextItem {
text,
x: text_matrix[4],
y: text_matrix[5],
width: 0.0, // Would need glyph widths
height: current_font_size,
height: rendered_size,
font: current_font.clone(),
font_size: current_font_size,
font_size: rendered_size,
page: page_num,
});
}
@@ -202,14 +203,15 @@ fn extract_page_text_items(
}
}
if !combined_text.trim().is_empty() {
let rendered_size = effective_font_size(current_font_size, &text_matrix);
items.push(TextItem {
text: combined_text,
x: text_matrix[4],
y: text_matrix[5],
width: 0.0,
height: current_font_size,
height: rendered_size,
font: current_font.clone(),
font_size: current_font_size,
font_size: rendered_size,
page: page_num,
});
}
@@ -225,14 +227,15 @@ fn extract_page_text_items(
extract_text_from_operand(&op.operands[0], doc, &fonts, &current_font)
{
if !text.trim().is_empty() {
let rendered_size = effective_font_size(current_font_size, &text_matrix);
items.push(TextItem {
text,
x: text_matrix[4],
y: text_matrix[5],
width: 0.0,
height: current_font_size,
height: rendered_size,
font: current_font.clone(),
font_size: current_font_size,
font_size: rendered_size,
page: page_num,
});
}
@@ -255,6 +258,19 @@ fn get_number(obj: &Object) -> Option<f32> {
}
}
/// Compute effective font size from base size and text matrix
/// Text matrix is [a, b, c, d, tx, ty] where a,d are scale factors
fn effective_font_size(base_size: f32, text_matrix: &[f32; 6]) -> f32 {
// The scale factor is typically the magnitude of the transformation
// For most PDFs, text_matrix[0] (a) is the horizontal scale
// and text_matrix[3] (d) is the vertical scale
let scale_x = (text_matrix[0].powi(2) + text_matrix[1].powi(2)).sqrt();
let scale_y = (text_matrix[2].powi(2) + text_matrix[3].powi(2)).sqrt();
// Use the larger of the two scales (usually they're equal for non-rotated text)
let scale = scale_x.max(scale_y);
base_size * scale
}
/// Extract text from a text operand, handling encoding
fn extract_text_from_operand(
obj: &Object,