pdftoolkit / pdf-reader
An explicit, bounded object reader for ISO 32000-1 PDF 1.7
Requires
- php: ^8.3
- ext-ctype: *
- ext-zlib: *
- roukmoute/pdf: ^0.2 || ^0.3 || ^0.4 || ^0.5
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.0
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^12.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is not auto-updated.
Last update: 2026-10-14 07:14:06 UTC
README
English | Français
A PHP reader for external PDFs, built around the objects of ISO 32000-1:2008. It exposes document dictionaries, arrays, references and streams with explicit resolution. Requires PHP 8.3 or newer; CI runs checks on PHP 8.3, 8.4 and 8.5.
This repository complements PDFToolkit/pdf,
which provides the primitives and writer. pdftoolkit/pdf-reader depends on
that model; the model does not depend on the reader. Both packages use the
same objects for names, strings, numbers, arrays, dictionaries and identifiers.
Installation
The package is available on Packagist. In your application:
composer require "pdftoolkit/pdf-reader:^0.1"
The reader uses roukmoute/pdf:^0.2 || ^0.3 || ^0.4 || ^0.5, installed
automatically by Composer. PHP 8.3 or newer on a 64-bit platform and the ctype,
iconv and zlib extensions are required for this combination. The application's
composer.lock records the selected versions and revisions.
Read pages
Run this example from a reader checkout after composer install.
The application loads the file; the reader receives only its bytes.
<?php declare(strict_types=1); require __DIR__ . '/vendor/autoload.php'; use PDF\Reading\DocumentReader; use PDF\Reading\FileBytes; use PDF\Reading\PageTreeLimits; use PDF\Reading\PageTreeReader; $bytes = file_get_contents(__DIR__ . '/tests/Fixtures/page-tree/classic.pdf'); if ($bytes === false) { throw new RuntimeException('Could not read the fixture.'); } $document = (new DocumentReader())->read(new FileBytes($bytes)); $tree = (new PageTreeReader($document, new PageTreeLimits( maxDepth: 64, maxNodes: 10_000, maxPages: 5_000, )))->read(); printf("Validated page count: %d\n", count($tree)); foreach ($tree as $index => $page) { // These are the effective PDF values, after page-tree inheritance. $attributes = $page->attributes; printf( "Page %d; rotation: %d; normalized rotation: %d\n", $index + 1, $attributes->rotate->integer(), $attributes->normalizedRotation(), ); foreach ($attributes->resources->entries() as $entry) { printf("Resource category: %s\n", $entry->key->value); } }
PageTreeReader::read() traverses /Pages branches and /Page leaves in
/Kids order. It checks every /Count, /Parent relationship, descendant
identity and traversal limit. The resulting ReadPageTree is countable and
iterable; page(0) identifies its first page. Each ReadPage retains its
document, reference and source dictionary, then exposes its effective
PageAttributes: resources, MediaBox, CropBox and rotation.
DictionaryType::get() always returns the stored value: a reference remains
an ObjectReference. ReadDocument::resolve() loads that reference in the
relevant document and leaves a direct value unchanged. Previously read objects
are kept in a document-scoped cache; there is no global registry, singleton
or resolution hidden inside values.
Inherited attributes use the closest ancestor that defines them. A local resource dictionary replaces inherited resources without merging them. The page guide explains inheritance and distinguishes written values from effective values. Traversal does not load contents or every resource of each page.
Read stream bytes
PageContentsReader resolves /Contents into an ordered collection of encoded
streams. It receives the document and its own per-page stream limit.
Continuing the preceding example with the first page of this fixture:
use PDF\Reading\PageContentLimits; use PDF\Reading\PageContentsReader; $page = $tree->page(0); $contents = (new PageContentsReader($document, new PageContentLimits( maxStreams: 128, )))->read($page); foreach ($contents as $stream) { $encodedBytes = $stream->encodedBytes(); // Decoding is requested explicitly, under the document's shared budgets. $decodedBytes = $document->streamData($stream); }
A page with no /Contents, null or an empty array yields an empty collection.
Array streams keep their order, without automatic concatenation or decoding.
An EncodedStream retains its dictionary and original bytes; streamData()
requests decoding. The result remains bytes: drawing operators, fonts and
encodings are not interpreted to extract text or render a page.
Current coverage
| Mechanism | Available reading |
|---|---|
| Syntax | Escaped names and strings, integers and reals, booleans, null, arrays, dictionaries, indirect references. |
| Structure | PDF 1.0 to 1.7 headers; indirect objects and streams delimited by /Length, including indirect lengths. |
| Traditional xref | Subsections, offsets, generations, free objects and revisions linked by /Prev. |
| Xref streams | /W fields, /Index ranges, free, direct or compressed types; unknown types yield an unavailable object. |
| Object streams | /ObjStm members, with header, offset and expected object number validation. |
| Filters | FlateDecode, ASCIIHexDecode, ASCII85Decode and RunLengthDecode; explicit filter chains and parameters. |
| Predictors | TIFF 2 and PNG 10 to 15, with 8-bit components. |
| Resolution | A missing, free or different-generation object yields NullType. |
| Page tree | Complete ordered traversal, checks for every /Count and /Parent, rejection of cycles or repeated descendants, dedicated traversal limits. |
| Inheritance | Effective resources, MediaBox, CropBox and rotation; CropBox defaults to MediaBox and absent rotation defaults to zero. |
| Page contents | A stream or an array of streams, kept in order; separate reading and decoding. |
Encrypted files, hybrid /XRefStm references, PDF 2.0 and unimplemented codecs
are explicitly rejected. A stream using an unknown codec can be preserved in
encoded form; requesting decoding raises UnsupportedPdfFeature.
Import into the writer, rewriting, merging, text extraction, rendering, forms, signatures and PDF repair remain to be implemented. External references and streams reject serialisation: reusing their numbers in a new document would be incorrect without explicit import and remapping.
Development
composer install composer check composer audit --locked
Tests cover syntax, filters, structures, pages and budgets. The five structure fixtures and two page fixtures are generated with pypdf and qpdf, independently of the PHP writer; their bytes and results are checked when reproduced.
The reader validates a structure when it is loaded. Opening the catalog does not perform an exhaustive audit of every object in the file. Consult the security controls and limits before choosing budgets for an upload service.
Documentation
- Architecture and object boundaries
- Pages, inheritance and content streams
- Component reference
- Security controls
- Reporting policy
- Changelog
- Reader module documentation
- Shared documentation for objects, the writer and modules
- PDF 1.7 standard included in the model repository
MIT licence.