How pdf-inspector's Content Stream Operator State Machine Handles Tj, TJ, Td/TD, Tm, q, and Q
pdf‑inspector processes PDF content stream operators through a compact state machine in src/extractor/content_stream.rs that maintains graphics and text state across six core operators to extract structured text items and bounding boxes.
This article explains how the firecrawl/pdf‑inspector Rust library implements its operator-level content-stream parser. The state machine tracks matrices, fonts, and extraction flags while processing the six most common PDF text and graphics operators.
Graphics State Operators: q and Q
The PDF specification uses q (save graphics state) and Q (restore graphics state) to bracket transformations. In pdf‑inspector, these manipulate a dedicated stack rather than matrices directly.
q (Save Graphics State)
When the parser encounters q, it pushes a SavedGraphicsState snapshot onto gstate_stack. The snapshot captures:
- Current transformation matrix (CTM)
- Text and line matrices
- Line width, dash pattern, and miter limit
- Rendering intent and flatness tolerance
- Font reference, font size, and all text-state parameters
This implementation follows the PDF spec's stack semantics exactly. Multiple nested q operations create multiple stack entries.
Q (Restore Graphics State)
The Q operator pops the most recent SavedGraphicsState and restores every captured field. The parser validates that a saved state exists; mismatched Q operators without prior q are handled gracefully by checking stack length before popping.
These operators enable complex page compositions where temporary clipping or transformations apply only to specific content regions.
Text Positioning Operators: Td, TD, and Tm
Text positioning in PDF uses relative and absolute matrix operations. pdf‑inspector distinguishes Td/TD (relative moves) from Tm (absolute matrix setting).
Td and TD (Translate Text Position)
Both operators move the text position by offset (tx, ty) relative to the line matrix:
Td: Updatesline_matrixby adding the scaled offset, then copies the result totext_matrixTD: Performs the same translation and setstext_leading = -ty(the negative of the y-offset)
The source code distinguishes these variants by operator name in the match arm, applying the additional leading assignment only for TD. This dual handling appears in the same code block for efficiency.
Tm (Set Text Matrix)
The Tm operator accepts six numeric operands defining a full 3×2 transformation matrix. pdf‑inspector:
- Reads all six operands in order [a b c d e f]
- Writes them directly into
text_matrix - Copies the same matrix to
line_matrix
Copying to line_matrix ensures subsequent Td/TD operations calculate relative offsets from this new baseline. This matches the PDF specification's requirement that Tm establishes both the current text position and the line-start reference.
Text Showing Operators: Tj and TJ
These operators render actual glyph content. The state machine performs substantial work: width calculation, invisible-text filtering, Unicode extraction, matrix advancement, and TextItem emission.
Tj (Show String)
The Tj handler in src/extractor/content_stream.rs executes five sequential steps:
- Compute string width using
compute_string_width_tswith current font metrics - Check visibility: if
text_rendering_mode == 3(invisible) andinclude_invisibleis false, skip glyph extraction but still advance the matrix - Extract Unicode text via
extract_text_from_operandwhen visible or explicitly included - Advance
text_matrixby the computed width, ensuring subsequent operators position correctly - Emit
TextItemwith position transformed throughrise_adjusted(&text_matrix, …)composed with CTM
The rise_adjusted helper incorporates the Ts (text-rise) parameter for superscript/subscript positioning. The final matrix multiplication multiply_matrices(..., &ctm) converts from text space to user space coordinates.
Invisible text handling deserves special attention: when skipped, the parser sets skipped_invisible = true to signal downstream consumers that an OCR fallback may be appropriate.
TJ (Show String with Positioning)
The TJ operator processes an array of alternating strings and numeric adjustments. This enables fine-grained glyph positioning for kerning and layout effects. pdf‑inspector's implementation:
- Calculate space threshold from the font's space-width metric to detect word boundaries
- Iterate the operand array, accumulating sub-strings separated by large negative offsets (column gaps)
- Create
TextItementries for each sub-string, applying accumulated positioning adjustments - Advance
text_matrixby the total array width after processing all elements - Handle
ActualTextmarkup: whensuppress_glyph_extractionis set (accessibility replacement text), record the starting glyph matrix but emit noTextItem
The array-walking logic distinguishes positive adjustments (kerning) from large negative values (explicit gaps), using this to segment output into logical text runs.
State Machine Architecture
Understanding the surrounding infrastructure clarifies how these six operators interrelate.
Core State Variables
The parser maintains these fields while in_text_block is true (between BT/ET):
current_font,current_font_sizechar_spacing,word_spacingtext_rise,text_leadingtext_matrix,line_matrixtext_rendering_mode(controls visibility)
Graphics state variables persist outside text blocks: ctm (current transformation matrix) and gstate_stack.
Matrix Coordinate Flow
PDF coordinates transform through multiple spaces:
Glyph coordinates → text_matrix → CTM → user space → page coordinates
The multiply_matrices helper composes these transformations, while rise_adjusted applies the Ts parameter as a vertical translation before final composition.
Extraction Flags and Modes
Two boolean flags modify operator behavior:
include_invisible: When false, rendering mode 3 suppresses text extraction (but not matrix advancement)suppress_glyph_extraction: Set duringBDC…EMCmarked-content blocks withActualTextreplacement
These flags enable accessibility-aware extraction that respects document intent while preserving positional information for layout analysis.
Practical Usage Examples
Command-Line Extraction
The pdf2md binary exercises all operator handling automatically:
# Extract structured text with position data
pdf2md --json sample.pdf > output.json
# Enable invisible text extraction
pdf2md --json --include-invisible sample.pdf > output.json
The JSON output contains TextItem objects whose x and y fields reflect the complete transformation chain including Tm, Td, and CTM effects.
Library Integration
Direct Rust usage provides control over extraction options:
use pdf_inspector::{process_pdf_with_options, ProcessOptions};
fn main() -> Result<(), pdf_inspector::PdfError> {
let opts = ProcessOptions::default();
let (page_extractions, _, _, _) = process_pdf_with_options(
"sample.pdf",
&opts,
&pdf_inspector::FontCMaps::default(),
false, // include_invisible
&mut pdf_inspector::FontStyleCache::new(),
&mut pdf_inspector::FormWalkBudget::default(),
)?;
for (items, _, _) in page_extractions {
for item in items {
println!("{:.2},{:.2}: {}", item.x, item.y, item.text);
}
}
Ok(())
}
The process_pdf_with_options function invokes extract_page_text_items, which instantiates the content-stream state machine and populates the TextItem vector through the operator handling described above.
Debugging Operator Processing
Instrument the state machine to trace specific operators:
// Add before the operator match in content_stream.rs
log::debug!(
"op={} operands={:?} tm={:?} ctm={:?}",
op.operator, op.operands, text_matrix, ctm
);
Run with RUST_LOG=pdf_inspector::extractor::content_stream=debug to observe step-by-step state transitions.
Source File Reference
| File | Purpose |
|---|---|
src/extractor/content_stream.rs |
Operator state machine implementing q, Q, Td, TD, Tm, Tj, TJ handlers |
src/extractor/mod.rs |
Entry point calling extract_page_text_items per page |
src/lib.rs |
Public API exposing extraction functions |
Summary
- Graphics state stack:
qpushes snapshots togstate_stack;Qpops and restores CTM and all subsidiary state - Relative positioning:
Td/TDoffset the line matrix and synchronize text matrix;TDadditionally sets leading - Absolute positioning:
Tmdirectly sets text matrix and copies to line matrix baseline - String showing:
Tjemits singleTextItemwith width-based matrix advancement;TJprocesses arrays with positioning adjustments and gap detection - Invisible text: Rendering mode 3 skips extraction (when configured) but preserves positioning for subsequent operators
- Coordinate transformation: All positions compose text matrix with CTM through
multiply_matrices, withtext_riseapplied viarise_adjusted
Frequently Asked Questions
How does pdf-inspector handle nested q/Q operators without losing state?
The gstate_stack: Vec<SavedGraphicsState> supports arbitrary nesting. Each q pushes a complete snapshot; each Q pops the most recent. The vector structure naturally handles LIFO semantics, and the parser checks stack length before popping to tolerate malformed PDFs with unmatched Q operators.
Why does Tm copy its matrix to the line matrix?
Per the PDF specification, Tm establishes both the current text position and the reference point for subsequent relative moves. Copying to line_matrix ensures that following Td/TD operators calculate offsets from this new baseline rather than the previous line's start position.
What's the difference between invisible text (Tr=3) and ActualText suppression?
Invisible text (text_rendering_mode == 3) still represents rendered content that may be desired for extraction; pdf‑inspector provides include_invisible to control this. ActualText suppression occurs during marked-content blocks where the visible glyphs serve only as positioning placeholders for replacement text intended for accessibility tools—the parser records the position but skips glyph emission entirely.
How does TJ detect word or column boundaries in the positioning array?
pdf‑inspector calculates a space-threshold from font_widths data. Negative numeric adjustments in the TJ array below this threshold indicate explicit gaps. The parser accumulates sub-strings separated by large negative offsets, emitting separate TextItem objects for each segment while advancing the matrix by the total array width.
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 →