Every PDF page has a content stream — a sequence of operands and operators that, when executed in order, produce the page's visual appearance. If you have ever seen raw PDF source and wondered what BT /F1 12 Tf 72 720 Td (Hello) Tj ET means, this guide explains exactly that. Understanding content streams is essential for: debugging PDF rendering issues; building PDF generation tools; analysing malicious PDFs; understanding why text extraction sometimes produces wrong results; and working with low-level PDF manipulation. The content stream model is also how all PDF accessibility, layer, and graphics state machinery works — marked content sequences, graphics state saves/restores, and XObject references are all content stream constructs.

Content stream syntax and operator categories

A PDF content stream is a compressed stream (typically Flate/zlib) whose decompressed data is a sequence of tokens — operands followed by their operator. The syntax rule: operands come first, operator comes last. 72 720 Td means "move text position to (72, 720) using operator Td". Text operators — The BT (Begin Text) / ET (End Text) pair brackets all text painting operations: - /F1 12 Tf — Select font named F1 at 12pt - 72 720 Td — Move text position to x=72, y=720 - (Hello world) Tj — Paint string "Hello world" - [(Hello) -250 (world)] TJ — Paint string with kerning adjustments (negative = tighter) - 0 1 Tc Tw — Set character spacing and word spacing Path operators — For vector graphics: - m — Move to (start new subpath) - l — Line to - c / v / y — Bezier curves (cubic, with first/last handle on current point) - re — Rectangle (shorthand for four line operations) - S / s — Stroke path / close and stroke - f / F / f* — Fill path (nonzero / nonzero / even-odd winding rule) - B / b — Fill and stroke / close, fill and stroke - n / W / W* — No paint (end path without painting) / clip to path Graphics state operators: - q / Q — Save / restore graphics state (push/pop the state stack) - cm — Concatenate matrix (modify current transformation matrix) - w — Line width - J / j / M — Line cap / join / miter limit - d — Dash pattern - gs — Set graphics state parameters from a named ExtGState resource Colour operators: - CS / cs — Set stroke / fill colour space - SC / sc — Set stroke / fill colour values - G / g — Set grey stroke / fill (shorthand for DeviceGray) - RG / rg — Set RGB stroke / fill - K / k — Set CMYK stroke / fill - ri — Rendering intent XObject operator: - /Im0 Do — Draw (invoke) named XObject Im0 from the resource dictionary

The graphics state stack

The graphics state is a set of parameters that affect all subsequent rendering operations. When the content stream begins, the graphics state is initialised to defaults. Operators modify the current state; q saves the current state and Q restores the most recently saved state. The graphics state includes: - CTM (Current Transformation Matrix) — A 6-element matrix [a b c d e f] mapping user-space coordinates to device coordinates. Initially maps PDF user units (1/72 inch) to device pixels. - Clipping path — The current clip region; only content within this path is rendered. - Stroke colour — Used by S, s, B, b operators - Fill colour — Used by f, F, f*, B, b operators - Line width, cap, join, dash — Control stroke appearance - Font and font size — Set by Tf - Text state — Character spacing (Tc), word spacing (Tw), horizontal scaling (Tz), leading (TL), text rise (Ts), text rendering mode (Tr 0–7 — fill, stroke, fill+stroke, invisible, etc.) - Alpha (opacity) — The current alpha constant for transparency compositing - Blend mode — How painted content composites with what is behind it The q/Q pattern is used extensively for temporary state changes: change colour, paint content, restore the previous colour. Without Q, changes accumulate and affect subsequent operations unexpectedly — a common source of content stream bugs.

Reading and debugging content streams

Decompressing content streams — Most PDF content streams are Flate-compressed. To read raw content: qpdf: qpdf --qdf --object-streams=disable input.pdf output.qdf produces a human-readable PDF with all streams decompressed and objects on individual lines. Python with pikepdf:
import pikepdf
pdf = pikepdf.open("input.pdf")
page = pdf.pages[0]
stream = page["/Contents"]
print(stream.read_bytes().decode("latin-1"))
pdftk: pdftk input.pdf output output.pdf uncompress decompresses content streams. Common content stream problems: Unbalanced q/Q — Every q must have a matching Q. Unbalanced saves/restores cause rendering state to leak between operations, producing unexpected colours, transforms, or clip regions on subsequent objects or pages. Missing resource references — If the content stream references /F1 but the page's /Resources /Font dictionary has no /F1 entry, the viewer may substitute a default font or produce an error. Coordinate system confusion — PDF's default coordinate origin is bottom-left, with y increasing upward. Most document formats (HTML, Word) use top-left origin with y increasing downward. Content streams generated by tools that use screen coordinates may appear flipped. The cm operator or a reflected CTM can re-orient the coordinate system. Text extraction order — Text operators in the content stream are not required to appear in reading order. They may be placed in any order for rendering efficiency. Screen readers and text extraction tools must sort by position or rely on the structure tree to determine reading order.