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
- GitHub: https://github.com/tecnickcom/tc-lib-pdf-parser
- API docs: https://tcpdf.org/docs/srcdoc/tc-lib-pdf-parser
- Packagist: https://packagist.org/packages/tecnickcom/tc-lib-pdf-parser
Project Metadata
| Item | Value |
|---|---|
| Namespace | \Com\Tecnick\Pdf\Parser |
| License | GNU 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
/Prevchains 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
| Option | Default | Description |
|---|---|---|
decode_streams | true | Decode 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_errors | false | Keep a stream that fails to decode as raw data instead of raising an exception. |
max_stream_size | 33554432 | Maximum size in bytes of a single decoded stream; 0 means unlimited. |
max_resolution_depth | 64 | Maximum number of indirect object resolutions in flight at once; values below 1 are clamped to 1. |
max_nesting_depth | 256 | Maximum nesting depth of array and dictionary objects; values below 1 are clamped to 1. |
strict_limits | false | Raise 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
/Indexhandling 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. #xxname 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
/Indexthat 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
/Lengthcovers 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_sizewhen 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. Setstrict_limitsin 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
- Sponsor: https://github.com/sponsors/tecnickcom
- Contribution guide: https://github.com/tecnickcom/tc-lib-pdf-parser/blob/main/CONTRIBUTING.md
- Security policy: https://github.com/tecnickcom/tc-lib-pdf-parser/blob/main/SECURITY.md