Position-Aware Text Extraction in pdf-inspector: How Font Metrics and Coordinates Preserve PDF Layout
pdf-inspector performs position-aware text extraction by parsing PDF content-stream operators to maintain a text position matrix, resolving glyph metrics from embedded fonts, and converting each character into a TextItem struct with absolute bounding box coordinates.
Position-aware text extraction is essential for converting PDF documents into structured formats without losing spatial layout information. The firecrawl/pdf-inspector repository implements a Rust-based extraction engine that interprets raw PDF drawing commands alongside font metric data to reconstruct the original reading order. By tracking the text matrix and calculating glyph advances using embedded font specifications, the tool generates precise coordinates for every text element.
The Position-Aware Text Extraction Pipeline
According to the firecrawl/pdf-inspector source code, the extraction process consists of three tightly-coupled stages that transform raw PDF operators into positioned text objects.
Stage 1: Parsing the Content Stream
The ContentStream state machine in src/extractor/content_stream.rs walks the PDF page’s stream of drawing operators including Tj, TJ, Td, Tm, and BT/ET. It maintains two critical matrices: the text matrix (Tm) and the text line matrix (Tlm). These matrices encode the current cursor position in user space coordinates, updating whenever positioning operators like Td (translate text position), Tm (set text matrix), or T* (move to next line) appear in the stream.
Stage 2: Resolving Fonts and Glyph Metrics
When a Tf operator selects a font, the extractor loads the corresponding Font object from src/extractor/fonts.rs. This module handles TrueType, Type0, and Base-14 fonts, calculating each glyph’s advance width, ascent, descent, and bounding box. The resolution process uses either the embedded CMap, the ToUnicode map, or a fallback NFKC-decoded glyph name to populate the GlyphInfo struct. These metrics are cached and made available via font.get_glyph_info(ch), which returns the data necessary to compute how far the text cursor should advance after drawing the character.
Stage 3: Converting Glyphs to Positioned Text Items
For every string emitted by Tj or TJ operators, the extractor iterates over the UTF-8 characters and looks up each glyph’s width. The parser calculates the text advance by multiplying the glyph width by the current font size and horizontal scaling factor. It then updates the Tm matrix with a translation matrix (Tm = Tm * Translate(advance, 0)). The resulting absolute origin coordinates, combined with the font-specific bounding box, populate a TextItem struct defined in src/types.rs. This struct carries the text string, font size, style information, and a PdfRect containing the precise x0, y0, x1, y1 coordinates.
From PDF Operators to Markdown: The Execution Flow
The pipeline connects these stages through a seven-step execution sequence:
-
Opening a page – The
pdf2mdbinary callsprocess_pdf_with_optionsinsrc/lib.rs, which creates aPdfDocumentand iterates over its pages. -
Content-stream extraction – For each page,
extractor/mod.rsinvokesContentStream::parse, feeding it the raw byte stream. The parser updatesTmandTlmon every positioning operator. -
Font handling – When encountering a
Tfoperator,fonts.rsloads the font object and caches its glyph metrics, making them available for subsequent character processing. -
Position calculation – The parser multiplies the glyph width by the current font size and horizontal scaling to obtain the text advance in user space. It applies this translation to
Tm, yielding the absolute origin for the next glyph. -
Storing items – Each glyph or word cluster emits as a
TextItem { text, position: PdfRect { x0, y0, x1, y1 }, font, size }. These items collect into a per-page vector. -
Layout reconstruction –
src/extractor/layout.rsgroupsTextIteminstances intoTextLinestructures by scanning vertical positions within a tolerance derived from the median line height. It uses histogram-based column detection to assign lines to columns, preserving multi-column and tabular layouts. -
Conversion to Markdown – The markdown pipeline in
src/markdown/convert.rsconsumes the orderedTextLineobjects, applies classification for headers and lists, and emits a token-efficient Markdown representation that reflects the original page geometry.
Code Examples: Accessing Position Data
You can extract position-aware text using the Rust API or the command-line interface.
Rust API
Configure PdfProcessOptions to include positional data and process the document:
use pdf_inspector::{process_pdf_with_options, PdfProcessOptions};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let opts = PdfProcessOptions {
include_positions: true,
..Default::default()
};
let result = process_pdf_with_options("example.pdf", opts)?;
for (page_no, items) in result.pages.iter().enumerate() {
println!("--- Page {} ---", page_no + 1);
for item in items {
println!(
"Text: \"{}\" x:{:.2} y:{:.2} w:{:.2} h:{:.2}",
item.text,
item.rect.x0,
item.rect.y0,
item.rect.x1 - item.rect.x0,
item.rect.y1 - item.rect.y0
);
}
}
Ok(())
}
Command Line
Run the pdf2md binary with the --json flag to receive structured output containing coordinates:
pdf2md --json sample.pdf > output.json
The JSON output contains x0, y0, x1, and y1 fields for each text element, derived from the font metric calculations described above.
Summary
- Position-aware text extraction in pdf-inspector relies on parsing PDF content streams to maintain the text matrix (
Tm) and text line matrix (Tlm). - The
src/extractor/fonts.rsmodule resolves glyph metrics including advance widths and bounding boxes from embedded font data. - Each character’s position is calculated by multiplying glyph width by font size and horizontal scaling, then updating the transformation matrix.
TextItemstructs insrc/types.rsstore absolute coordinates inPdfRectobjects, enabling precise layout reconstruction.- The
src/extractor/layout.rsmodule clusters positioned items into lines and columns, preserving complex layouts like tables and multi-column documents.
Frequently Asked Questions
What is the difference between the text matrix (Tm) and the text line matrix (Tlm)?
The text matrix (Tm) tracks the current position for the next glyph to be rendered, updating with every character advance. The text line matrix (Tlm) establishes the starting position of the current line and only changes when line-breaking operators like Td or T* are encountered. According to the firecrawl/pdf-inspector source code, Tlm provides the reference point for line-relative positioning while Tm handles intra-character spacing.
How does pdf-inspector handle fonts that lack embedded metric data?
When processing fonts in src/extractor/fonts.rs, the extractor attempts to resolve glyph metrics using the embedded CMap or ToUnicode map. If these are unavailable, it falls back to NFKC-decoded glyph names to estimate the advance width and bounding box. This ensures that even PDFs with subsetted or poorly embedded fonts produce reasonable position estimates for text extraction.
Can pdf-inspector detect tables and multi-column layouts?
Yes. Because every TextItem carries precise (x, y) coordinates, src/extractor/layout.rs can identify column breaks by detecting large horizontal gaps between consecutive items. The module uses histogram-based clustering to align items into grid structures, enabling accurate table detection and preservation of multi-column newspaper layouts during Markdown conversion.
What coordinate system does pdf-inspector use for positioning?
pdf-inspector uses user space coordinates as defined by the PDF specification. The Tm and Tlm matrices operate in this coordinate system, which is then transformed by the page's current transformation matrix to produce the final absolute positions stored in PdfRect. These coordinates reflect the original PDF's spatial layout, typically with the origin at the bottom-left or top-left depending on the page's crop box configuration.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →