# Position-Aware Text Extraction in pdf-inspector: How Font Metrics and Coordinates Preserve PDF Layout

> Discover how pdf-inspector achieves position-aware text extraction using font metrics and coordinates. Learn to preserve PDF layout accurately with our advanced techniques.

- Repository: [Firecrawl/pdf-inspector](https://github.com/firecrawl/pdf-inspector)
- Tags: internals
- Published: 2026-08-11

---

**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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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:

1. **Opening a page** – The `pdf2md` binary calls `process_pdf_with_options` in [`src/lib.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/lib.rs), which creates a `PdfDocument` and iterates over its pages.

2. **Content-stream extraction** – For each page, [`extractor/mod.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/extractor/mod.rs) invokes `ContentStream::parse`, feeding it the raw byte stream. The parser updates `Tm` and `Tlm` on every positioning operator.

3. **Font handling** – When encountering a `Tf` operator, [`fonts.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/fonts.rs) loads the font object and caches its glyph metrics, making them available for subsequent character processing.

4. **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.

5. **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.

6. **Layout reconstruction** – [`src/extractor/layout.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/layout.rs) groups `TextItem` instances into `TextLine` structures 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.

7. **Conversion to Markdown** – The markdown pipeline in [`src/markdown/convert.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/markdown/convert.rs) consumes the ordered `TextLine` objects, 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:

```rust
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:

```bash
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.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/fonts.rs) module 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.
- `TextItem` structs in [`src/types.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/types.rs) store absolute coordinates in `PdfRect` objects, enabling precise layout reconstruction.
- The [`src/extractor/layout.rs`](https://github.com/firecrawl/pdf-inspector/blob/main/src/extractor/layout.rs) module 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`](https://github.com/firecrawl/pdf-inspector/blob/main/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`](https://github.com/firecrawl/pdf-inspector/blob/main/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.