Optional module · pdftoolkit/pdf-reader 0.1.7

English | Français

The reader transforms PDF bytes into a browsable object graph. It retains references as explicit values and uses primitives from PDFToolkit/pdf. The reference clauses are ISO 32000-1, 7.2–7.5 for structure and 7.7.3 for pages; the standard PDF remains in the model repository.

Two packages and one dependency

roukmoute/pdf defines primitives and their writing. pdftoolkit/pdf-reader adds reading mechanisms under PDF\Reading. Composer connects the reader to the model; there is no reverse dependency from the writer to the reader.

A parsed string becomes a StringType, a name a NameType, an array an ArrayType, and a dictionary a DictionaryType. Objects are not transformed into application associative arrays or JSON strings. PDF strings remain bytes; text interpretation belongs to layers that know the context and encoding.

Two types belong to the source document: ObjectReference identifies an object without carrying its target, and EncodedStream retains a dictionary and its encoded bytes. They implement BasicType so they can be used in the same arrays and dictionaries. Their toPdf() throws an exception: an import must rebuild references in the destination document before writing can be correct. No import bridge is implemented yet.

Reading pipeline

FileBytesDocumentReaderCrossReferenceReaderSection and IndexReadDocumentIndirectObjectReaderObjectStreamReaderStreamDecoderPageTreeReaderByteCursor and SyntaxParserDecoders and predictorsReadPageTree and ReadPagePageContentsReaderPageContents and EncodedStream

DocumentReader checks the header and final footer, reads the /Prev revision chain and assembles the effective index. It traverses sections from newest to oldest. A newer entry overrides earlier entries, including when it marks an object as free. The effective trailer also retains the newest values. The catalog is resolved and checked before read() returns the document.

CrossReferenceReader reads traditional tables or xref streams. An xref stream's structural fields must be direct: no resolver is available yet during index construction. Ranges are bounded, generations are checked and binary fields are read in big-endian order without integer overflow. Hybrid sections and encryption are rejected.

ReadDocument owns the source, index, limits and caches. It loads an object on demand, checks that its identifier matches the xref entry, then retains the same instance for later accesses. A missing, free or different-generation reference returns a NullType. Normal graph cycles such as /Parent and /Kids remain references; resolving an object does not automatically traverse its neighbours.

An identifier is local to its file. Resolve a reference using the ReadDocument it came from; the same number/generation pair may identify a different object in another document.

ByteCursor provides a bounded view of a byte string with absolute positions. SyntaxParser reads a syntax value without resolving references. Structural readers then add the meaning of obj, endobj, stream and dictionary entries. /Length determines the stream's binary extent: an endstream marker inside its data remains data.

ObjectStreamReader checks /N, /First and number/offset pairs. Each member is parsed in its own bounded view, then its number is compared with the xref entry identifying it. The container and its members have generation zero; a member cannot be a stream or a simple reference. /Extends describes a collection of streams and does not trigger an automatic search in another container.

Explicit resolution and decoding

ReadDocument::resolve() performs one operation: resolve a reference or preserve a direct value. The ObjectResolver interface lets readers that need this service, notably for /Length and filter parameters, receive it through construction or an argument.

EncodedStream::encodedBytes() retains the original bytes. ReadDocument::streamData() triggers decoding and retains the result in a document-scoped cache. StreamDecoder reads /Filter and /DecodeParms, applies the chain in PDF order, then predictors when present. Filters are separate, composable, testable objects.

Caches, cursors and counters are mutable because they represent an ongoing read. Identifiers, references, sections and limits are immutable values. No singleton, global state, static factory or service hidden in a value object is needed.

Pages as source document objects

PageTreeReader receives a ReadDocument and its PageTreeLimits. A read() request creates traversal state in PageTreeTraversal. A PageTreeFrame stack follows children in /Kids order and counts each branch's leaves on return. Previously visited references allow cycles and repeated descendants to be rejected; /Parent entries must match the parents actually traversed. The reader returns a ReadPageTree only after checking the entire tree's counts.

PageAttributeReader resolves the four inheritable entries using ReadDocument as its ObjectResolver. It produces partial PageInheritance values down to the leaf, where defaults become effective. This distinction prevents a default CropBox from being fixed to an ancestor's MediaBox when a page overrides that MediaBox lower in the tree. Resources are selected as a complete dictionary, without merging.

A ReadPage retains the document that owns its reference, its source dictionary and its effective PageAttributes. The latter retain PDF objects, including the raw rotation integer. Normalising orientation is a separate request; it does not rewrite source values.

PageContentsReader composes this service with a per-page stream limit. It resolves the local /Contents entry in the owning document and returns an ordered PageContents collection of EncodedStream values. It does not inherit this entry, concatenate streams or execute filters. Decoding remains a request to ReadDocument::streamData(), which uses its caches and budgets. The page guide explains this composition using independent fixtures.

Budgets and boundaries

ReaderLimits describes ceilings. A ReadBudget, created for each DocumentReader::read() call, adds up parsed values, xref entries, retained bytes and bytes produced by decoding stages. Ceilings are shared by readers of the same document; they do not restart at zero for each reference or filter.

The library receives FileBytes. The application selects the file, authorises reading, bounds loading and manages resources. JavaScript, URI and launch actions remain dictionaries; the library does not execute them. An external stream is never loaded from /F.

This stage provides syntax, structural resolution and page traversal. Future importers, content interpreters and form engines can compose them without adding those responsibilities to the parser.

Components · Security · README

Guide source at the documented commit ↗