A PDF is a painting program, not a document

This chapter looks inside Firecrawl's pdf-inspector to answer a specific question: how can native PDF bytes become evidence that later stages can place, order, and interpret? The answer is that pdf-inspector does not begin with a paragraph string. It walks the page's painting instructions, keeps the state that gives those instructions meaning, and emits a positioned intermediate representation. That representation is the foundation for the project's quality-aware decisions.

The instructions that paint a page

A PDF page is often described as if it contained a document in a ready-made form: a heading followed by paragraphs, then a table. Its internal representation is less literary. A page content stream is a sequence of operations that tells a renderer what to paint and where. Some operations begin or end a text block. Others select a font, move the text position, show characters, draw a rectangle, or apply a transformation. Images, paths, annotations, and forms can contribute to the page's visible result without being part of a plain text stream.

That difference explains why extracting bytes is not enough. Suppose a content stream selects a font, moves a text matrix to a location, applies a horizontal scale, and then shows a short byte sequence. The bytes alone do not say which Unicode characters they represent, how wide the run is, whether it is bold, or whether it belongs above the body line as a superscript. Those answers come from state accumulated before and around the text-showing operation.

pdf-inspector therefore walks the stream as a state machine. It tracks the graphics state, including the current transformation matrix and clipping information, alongside text state such as the text and line matrices, current font, font size, character spacing, word spacing, horizontal scaling, and text rise. It also tracks the rendering mode: text can be filled, stroked, both, or invisible. This is not bookkeeping for its own sake. Each state variable can change the geometry or the trustworthiness of the item emitted for a glyph run.

The current transformation matrix determines how page-space coordinates are affected by scaling, translation, reflection, or rotation. The text matrix determines where the next run begins, while the line matrix supports movement to a new line. Character and word spacing influence the advance from one run to the next. Horizontal scaling changes both the visible extent and the cursor's movement. Text rise can move an affiliation marker or exponent above the body baseline. A parser that ignores these relationships may recover familiar characters while placing them in the wrong column or separating a footnote marker from the sentence it annotates.

The same state boundary applies to non-text evidence. A rectangle may be a table cell, a chart bar, a background fill, or a clipping path that never becomes visible. A line segment can be part of a rule or a more complex drawing. A form XObject can contain nested text and graphics, and a page annotation can place a link or form field over the rendered page. pdf-inspector follows the relevant forms and records drawing geometry while distinguishing painted content from operations that only affect clipping or future painting.

This is the first architectural relationship to keep in mind: the content stream owns the sequence of painting instructions, the parser owns the evolving graphics and text state, and the emitted items carry the consequences of that state to later stages. Layout and Markdown do not have to replay the entire PDF program. They receive a structured account of what the parser observed.

sequenceDiagram participant Caller participant Parser as PDF parser participant State as Graphics and text state participant Fonts as Font resources participant Evidence as Positioned evidence participant Layout as Layout and Markdown Caller->>Parser: Supply PDF bytes Parser->>State: Read page operators State->>Fonts: Resolve font, encoding, and metrics Fonts-->>State: Return mapping, style, and advance data State->>Evidence: Emit positioned text items Parser->>Evidence: Emit rectangles, lines, links, and fields Evidence->>Layout: Provide reusable page evidence Layout-->>Caller: Return analyzed or formatted output

State decides what a glyph run means

A text-showing operation is the point where several kinds of state meet. First, the parser needs the font resource. A font may supply widths, style information, an encoding, or a reference to a ToUnicode map. The map may be direct, range-based, or absent; its details determine whether the byte sequence can become a meaningful character string. Font metrics determine how much space the run occupies. The text matrix and transformation state place that extent on the page. Rendering state determines whether the text is visible and whether a filled-and-stroked run carries emphasis.

The result is a TextItem, not merely a character. It includes the recovered text and a box with x/y position, width, and height. It also retains font identity and size, bold and italic information, underline or strikeout status, the page number, an item type, and optional marked-content identity. Newer geometry fields distinguish the angle of a run from its axis-aligned box and record whether its advance came from real font metrics or an estimate. A baseline shift records that a smaller run belongs to a larger line as a superscript or subscript.

These fields make later judgments possible. A line grouper can compare baselines rather than assuming every item's y-coordinate is a body baseline. A table detector can use widths and x-positions to assign values to cells. A layout detector can ignore an image placeholder when it builds a text-column profile. A Markdown converter can preserve a link or an emphasized run without guessing from the final string. The intermediate representation is valuable precisely because it keeps relationships that would disappear after flattening.

Marked content adds another route to meaning. Tagged PDFs can associate a marked-content identifier with a semantic role such as a heading, paragraph, table, or cell. pdf-inspector exposes the identifier on the text item and can resolve structure-tree entries to page and identifier pairs. That does not turn every PDF into an accessible document—many PDFs are untagged—but it gives the Markdown layer a stronger signal when the source provides one. Typography and position remain useful fallbacks when the semantic tree is absent or overused.

The parser also has to preserve what it does not know. If a font carries no width metrics, the item can retain an estimated extent and mark the advance as uncertain instead of quietly presenting the estimate as exact. If a page uses an invisible text layer, the public native path can suppress it while an OCR-aware path may retry it under a different policy. If a content stream exceeds bounded decompression or operation limits, the page can degrade into an extraction failure rather than forcing the process to keep expanding work.

That approach reflects a broader rule: uncertainty belongs in the intermediate evidence when it can affect a downstream decision. A later stage cannot make a better judgment if the parser has already discarded whether a width was measured, whether text was invisible, or whether a semantic tag was present.

Why geometry is the reusable interface

The most important output of native extraction is not any individual field. It is the common coordinate frame that lets different consumers ask different questions about the same page. pdf-inspector's public positioned APIs use the visible page box: the intersection of the CropBox and MediaBox when a CropBox exists, otherwise the MediaBox. The lower-left corner of that box is the origin, and y increases upward.

Rendered page images and region APIs use the same visible box but read it from the top-left, with y increasing downward. The conversion therefore depends on the visible box's height. A consumer can flip a positioned y-coordinate by subtracting it from that height, with the text height included when it needs a glyph band rather than a baseline. This contract matters whenever a layout model proposes a region in image coordinates and the application wants to recover native text inside it.

Without a shared frame, a CropBox offset can create a silent error. A renderer may show only the visible portion of a page starting at its own top-left origin, while an extractor returns raw content-stream coordinates measured from a different origin. The two boxes can look similar but fail to intersect the same words. pdf-inspector shifts text, images, links, and form fields consistently into the public visible-page-box frame. The parser keeps raw user-space geometry internally where its own Markdown edge heuristics depend on it, but it gives positional consumers a frame they can join to rendered evidence.

Orientation adds another condition. The /Rotate page attribute is not automatically applied. When the text itself is predominantly rotated, pdf-inspector can turn the coordinate frame so the dominant text reads left to right and report the page rotation. A vertical margin stamp or chart-axis title should retain a tall, narrow box and a rotation angle; it should not be turned into a zero-width horizontal line merely because an older consumer used width as a rotation signal. Mirrored and upside-down text, right-to-left runs, and diagonal or deskewed text require their own direction and geometry decisions.

The same principle explains baseline-aware scripts. An affiliation marker may sit several points above the author line, and an exponent may be smaller and lower than the formula. If line grouping uses only the raw baseline of every run, these markers can become orphan lines. pdf-inspector records a baseline shift and provides a body-line baseline so grouping, table-cell assignment, and later formatting can keep the marker attached to the text it annotates.

Geometry also gives one page several possible consumers without duplicating parsing work. A region extractor can select text inside a bounding box. A table detector can match text items to a grid of rectangles. A column detector can analyze x-occupancy and y-overlap. A structure-tree consumer can join semantic roles by page and marked-content identifier. An OCR fusion layer can map recognition polygons back into the same PDF coordinate system. Each consumer asks a different question, but all depend on the parser preserving a common spatial account.

One parsed document, three levels of work

The high-level pipeline uses this intermediate evidence to make processing proportional. It validates the input, loads the document once, and shares the parsed document between detection and extraction. That avoids making the detector and the full extractor independently repeat the most expensive part of understanding the PDF. The caller can then choose how far the pipeline should proceed.

In detection-only mode, the goal is classification metadata and routing information, not text or Markdown. In analysis mode, pdf-inspector adds extraction and layout analysis without generating the final Markdown. In full mode, it continues through structure reconstruction and formatting. The distinction is useful because an ingestion system may need a quick page decision before it commits to more work, while a debugging or quality service may need positions and layout metadata without yet choosing a presentation format.

Page selection is another boundary, but it does not mean that every decision becomes isolated from the rest of the document. The per-page Markdown path can compute font statistics and page-number context using document-wide evidence before returning only the requested pages. That gives a selected page a consistent basis for heading and furniture decisions. At the same time, errors on a requested page remain meaningful, while a context-only page can be skipped in the specific analysis path when it is malformed.

The output can therefore be assembled in stages: detection supplies a type and page-level signals; native extraction supplies positioned text and drawing geometry; layout and Markdown consume that evidence; and a caller can inspect per-page needs_ocr or complexity metadata before deciding whether to continue. The boundaries cross through explicit results rather than hidden assumptions about one giant text string.

The practical consequence is that a parser can be fast without pretending that speed solves interpretation. The content stream establishes what was painted and how. State turns operations into measurable items. The coordinate contract makes those items joinable. Processing modes let an application stop at the level it needs. But a correctly positioned item can still carry the wrong character if its font mapping is broken. The next question is therefore not where the glyph is, but whether the text attached to that position deserves to be believed.

References