fix(extractor): Include CTM scaling in font_size to fix two-column merge

effective_font_size() was computed from the text matrix alone, ignoring
CTM scaling. In PDFs with a small CTM scale (e.g. 0.24×) and large Tm
values (e.g. 58), font_size was inflated (58pt instead of ~14pt), causing
the merge threshold to bridge inter-column gaps and merge two-column
text into single lines. Now compute the combined matrix (text_matrix × CTM)
before calling effective_font_size() at all 6 call sites.

Also adds PdfRect extraction from `re` operators for future table-grid
detection, and related plumbing changes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-02-17 12:59:09 -08:00
co-authored by Claude Opus 4.6
parent d35adf22d9
commit b547e685de
4 changed files with 381 additions and 43 deletions
+33 -18
View File
@@ -17,9 +17,11 @@ pub use detector::{
detect_pdf_type_with_config, DetectionConfig, PdfType, PdfTypeResult, ScanStrategy,
};
pub use extractor::{
extract_text, extract_text_with_positions, extract_text_with_positions_pages, TextItem,
extract_text, extract_text_with_positions, extract_text_with_positions_pages, PdfRect, TextItem,
};
pub use markdown::{
to_markdown, to_markdown_from_items, to_markdown_from_items_with_rects, MarkdownOptions,
};
pub use markdown::{to_markdown, to_markdown_from_items, MarkdownOptions};
use std::path::Path;
@@ -66,8 +68,9 @@ pub fn process_pdf<P: AsRef<Path>>(path: P) -> Result<PdfProcessResult, PdfError
let result = match pdf_type {
PdfType::TextBased => {
// Step 2: Full extraction with position-aware reading order
let items = extract_text_with_positions(&path)?;
let markdown = to_markdown_from_items(items, MarkdownOptions::default());
let (items, rects) = extractor::extract_text_with_positions_and_rects(&path, None)?;
let markdown =
to_markdown_from_items_with_rects(items, MarkdownOptions::default(), &rects);
PdfProcessResult {
pdf_type,
@@ -95,8 +98,10 @@ pub fn process_pdf<P: AsRef<Path>>(path: P) -> Result<PdfProcessResult, PdfError
}
PdfType::Mixed => {
// Try to extract what we can with position-aware reading order
let items = extract_text_with_positions(&path).ok();
let markdown = items.map(|i| to_markdown_from_items(i, MarkdownOptions::default()));
let result = extractor::extract_text_with_positions_and_rects(&path, None).ok();
let markdown = result.map(|(items, rects)| {
to_markdown_from_items_with_rects(items, MarkdownOptions::default(), &rects)
});
PdfProcessResult {
pdf_type,
@@ -146,8 +151,9 @@ pub fn process_pdf_with_config_pages<P: AsRef<Path>>(
let result = match pdf_type {
PdfType::TextBased => {
let items = extract_text_with_positions_pages(&path, page_filter)?;
let markdown = to_markdown_from_items(items, markdown_options);
let (items, rects) =
extractor::extract_text_with_positions_and_rects(&path, page_filter)?;
let markdown = to_markdown_from_items_with_rects(items, markdown_options, &rects);
PdfProcessResult {
pdf_type,
@@ -171,8 +177,10 @@ pub fn process_pdf_with_config_pages<P: AsRef<Path>>(
confidence,
},
PdfType::Mixed => {
let items = extract_text_with_positions_pages(&path, page_filter).ok();
let markdown = items.map(|i| to_markdown_from_items(i, markdown_options.clone()));
let result = extractor::extract_text_with_positions_and_rects(&path, page_filter).ok();
let markdown = result.map(|(items, rects)| {
to_markdown_from_items_with_rects(items, markdown_options.clone(), &rects)
});
PdfProcessResult {
pdf_type,
@@ -207,8 +215,10 @@ pub fn process_pdf_mem(buffer: &[u8]) -> Result<PdfProcessResult, PdfError> {
let result = match pdf_type {
PdfType::TextBased => {
// Step 2: Full extraction with position-aware reading order
let items = extractor::extract_text_with_positions_mem(buffer)?;
let markdown = to_markdown_from_items(items, MarkdownOptions::default());
let (items, rects) =
extractor::extract_text_with_positions_mem_and_rects(buffer, None)?;
let markdown =
to_markdown_from_items_with_rects(items, MarkdownOptions::default(), &rects);
PdfProcessResult {
pdf_type,
@@ -232,8 +242,10 @@ pub fn process_pdf_mem(buffer: &[u8]) -> Result<PdfProcessResult, PdfError> {
confidence,
},
PdfType::Mixed => {
let items = extractor::extract_text_with_positions_mem(buffer).ok();
let markdown = items.map(|i| to_markdown_from_items(i, MarkdownOptions::default()));
let result = extractor::extract_text_with_positions_mem_and_rects(buffer, None).ok();
let markdown = result.map(|(items, rects)| {
to_markdown_from_items_with_rects(items, MarkdownOptions::default(), &rects)
});
PdfProcessResult {
pdf_type,
@@ -270,8 +282,9 @@ pub fn process_pdf_mem_with_config(
let result = match pdf_type {
PdfType::TextBased => {
let items = extractor::extract_text_with_positions_mem(buffer)?;
let markdown = to_markdown_from_items(items, markdown_options);
let (items, rects) =
extractor::extract_text_with_positions_mem_and_rects(buffer, None)?;
let markdown = to_markdown_from_items_with_rects(items, markdown_options, &rects);
PdfProcessResult {
pdf_type,
@@ -295,8 +308,10 @@ pub fn process_pdf_mem_with_config(
confidence,
},
PdfType::Mixed => {
let items = extractor::extract_text_with_positions_mem(buffer).ok();
let markdown = items.map(|i| to_markdown_from_items(i, markdown_options.clone()));
let result = extractor::extract_text_with_positions_mem_and_rects(buffer, None).ok();
let markdown = result.map(|(items, rects)| {
to_markdown_from_items_with_rects(items, markdown_options.clone(), &rects)
});
PdfProcessResult {
pdf_type,