Search by

pdftoolkit / pdf-reader

roukmoute

An explicit, bounded object reader for ISO 32000-1 PDF 1.7

v0.1.6 2026-10-14 07:10 UTC

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

MIT licence.