feat: extract invisible OCR text layer from Mixed/template PDFs

Scanned PDFs with OCR text layers use rendering mode 3 (invisible text)
positioned behind page images. Previously we skipped all Tr=3 text.

Now for Mixed/template PDFs, if normal extraction produces garbage or
empty output, we retry with invisible text included. This unlocks text
from OCR-generated PDFs without requiring external OCR.

Also adds Windows Unicode BMP (3,1) subtable support to the TrueType
cmap fallback, and allows TrueType fonts with explicit encoding to
extract their embedded cmap (OCR fonts often lie about encoding).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Abimael Martell
2026-03-18 11:39:38 -07:00
co-authored by Claude Opus 4.6
parent 8e674eeef5
commit b8211db4b0
4 changed files with 94 additions and 13 deletions
+20 -1
View File
@@ -149,6 +149,25 @@ pub(crate) fn extract_positioned_text_from_doc(
doc: &Document,
font_cmaps: &FontCMaps,
page_filter: Option<&HashSet<u32>>,
) -> Result<(PageExtraction, PageThresholds), PdfError> {
extract_positioned_text_impl(doc, font_cmaps, page_filter, false)
}
/// Extract with option to include invisible (Tr=3) text.
/// Used for Mixed/template PDFs where the OCR text layer is invisible.
pub(crate) fn extract_positioned_text_include_invisible(
doc: &Document,
font_cmaps: &FontCMaps,
page_filter: Option<&HashSet<u32>>,
) -> Result<(PageExtraction, PageThresholds), PdfError> {
extract_positioned_text_impl(doc, font_cmaps, page_filter, true)
}
fn extract_positioned_text_impl(
doc: &Document,
font_cmaps: &FontCMaps,
page_filter: Option<&HashSet<u32>>,
include_invisible: bool,
) -> Result<(PageExtraction, PageThresholds), PdfError> {
let pages = doc.get_pages();
let mut all_items = Vec::new();
@@ -167,7 +186,7 @@ pub(crate) fn extract_positioned_text_from_doc(
}
}
let (mut items, rects, lines) =
extract_page_text_items(doc, page_id, *page_num, font_cmaps)?;
extract_page_text_items(doc, page_id, *page_num, font_cmaps, include_invisible)?;
let threshold = crate::text_utils::fix_letterspaced_items(&mut items);
if threshold > 0.10 {
page_thresholds.insert(*page_num, threshold);