tc-lib-pdf-parser

Technical overview and integration notes for tc-lib-pdf-parser

Overview

tc-lib-pdf-parser parses raw PDF data into structured PHP arrays for extraction, analysis, and downstream processing.

It is built for tooling: inspecting content, pulling out metadata, checking structure, and feeding migration pipelines. It also backs the page import in tc-lib-pdf.

Repository and API Docs

Project Metadata

ItemValue
Namespace\Com\Tecnick\Pdf\Parser
LicenseGNU LGPL v3

Installation

composer require tecnickcom/tc-lib-pdf-parser

Where It Fits

When PDFs arrive from outside and have to be read: analysed, merged, mined for content, or stamped and passed on.

Features

Parsing Capabilities

  • Cross-reference tables and cross-reference streams, including /Prev chains and incremental updates
  • Object streams (/ObjStm)
  • Indirect references, resolved on demand and bounded against cycles
  • Stream decoding through tc-lib-pdf-filter, including the predictors of DecodeParms
  • Structured output suitable for custom extractors

Runtime Design

  • Configuration options for tolerant parsing modes
  • Pure-PHP parser with no external service dependency
  • Typed exceptions for error handling

Parser Options

OptionDefaultDescription
decode_streamstrueDecode object stream content while parsing. Set it to false when the caller only needs the raw stream bytes (for example when importing pages), which avoids the cost of decoding streams that are re-emitted unchanged.
ignore_filter_errorsfalseKeep a stream that fails to decode as raw data instead of raising an exception.
max_stream_size33554432Maximum size in bytes of a single decoded stream; 0 means unlimited.
max_resolution_depth64Maximum number of indirect object resolutions in flight at once; values below 1 are clamped to 1.
max_nesting_depth256Maximum nesting depth of array and dictionary objects; values below 1 are clamped to 1.
strict_limitsfalseRaise a LimitException as soon as a limit or a reference cycle leaves an object unresolved.

Limits

Parsing is bounded on every axis a hostile document can grow:

  • Maximum decoded size of a single stream (max_stream_size)
  • Maximum nesting depth of arrays and dictionaries (max_nesting_depth)
  • Maximum depth of indirect object resolutions (max_resolution_depth), with object reference cycles broken
  • Maximum number of chained cross-reference sections (1024)

Exceeding max_nesting_depth always raises Com\Tecnick\Pdf\Parser\LimitException, a subclass of Com\Tecnick\Pdf\Parser\Exception: a dictionary or array that cannot be tokenized has no usable value to fall back to.

Reaching max_resolution_depth, or meeting a reference cycle, leaves that reference unresolved and lets the rest of the document parse. Both cases are recorded and readable after parse(), one description per kind of event, with the number of occurrences and the first object affected:

$parser = new \Com\Tecnick\Pdf\Parser\Parser();
[$xref, $objects] = $parser->parse((string) $raw);

foreach ($parser->getLimitWarnings() as $warning) {
    // "the indirect object resolution depth limit (64) left a reference
    //  unresolved 7 times, first at object 128_0"
}

With strict_limits the first such event raises a LimitException instead, and getLimitWarnings() then always returns an empty list.

Robustness

  • Cross-reference stream /Index handling and the stream predictors are fully implemented, with predictor geometry and row coverage validated.
  • An out-of-range startxref, free entries, literal strings and comments in the trailer are handled without aborting the parse.
  • #xx name escapes are decoded, hex strings validated, and unbalanced delimiters and unterminated containers tokenized without spinning.
  • Dictionary and array parse loops bail out when the offset stops advancing, so malformed input cannot spin forever.
  • The cross-reference stream is accepted only where the tokenizer reads an actual indirect object header, and a declared /Index that is not an array of numbers is refused instead of being replaced by the default coverage.
  • An object body taken from an object stream is tokenized as it stands, so a reference to its own object number is not mistaken for a cycle.
  • A stream whose /Length covers every extracted byte keeps its full payload.
  • Partially damaged documents still yield a usable object tree.

Integration Notes

  • Treat external PDFs as untrusted input and validate before processing; lower max_stream_size when the expected documents are small.
  • Read getLimitWarnings() after every parse: an unresolved reference means the object tree is incomplete, and only the caller knows whether that matters. Set strict_limits in the pipelines where it never does.
  • Catch the parser exceptions where you can attribute them to a document. A stack trace with no filename is worth very little when a batch of ten thousand fails on one.
  • Build fixtures from PDFs you have actually received. Real-world producers emit structures the specification permits and nobody writes by hand, and those are what break a parser.

Requirements

  • PHP 8.2 or later
  • Extension: pcre
  • Package dependency: tecnickcom/tc-lib-pdf-filter
  • Composer

Example

<?php

require_once __DIR__ . '/vendor/autoload.php';

$raw = file_get_contents('/path/to/document.pdf');
$parser = new \Com\Tecnick\Pdf\Parser\Parser([
    'ignore_filter_errors' => true,
    'max_stream_size' => 16 * 1024 * 1024,
]);
$data = $parser->parse((string) $raw);

var_dump($data);

Development and Packaging

  • QA and local checks: make deps, make help, make qa
  • Coverage report: make qa-coverage
  • Packaging: make rpm, make deb

Support and Contribution