Pro edition
Writer — Deep Reference
At a glance
Section titled “At a glance”The Writer module writes PDF incremental-update revisions and packs small objects into Object Streams. The incremental writer enforces a fail-closed append-only rule: every byte the buffer held before a revision must remain unchanged after it. The Object Stream builder groups eligible objects into one FlateDecode-compressed /Type /ObjStm object under a bounded size.
Availability & licensing
Section titled “Availability & licensing”This capability ships in NextPDF Pro (nextpdf/pro) and activates with a Pro-tier license envelope. A deployment without that entitlement does not load the capability’s classes. Compare editions and get a license. There is no per-feature license flag; the code ships with the Pro edition.
Public API surface
Section titled “Public API surface”The module lives under the NextPDF\Pro\Writer namespace. All public symbols are listed below. Value objects are immutable final readonly classes.
| Symbol | Parameters | Default behavior | Returns | Throws or fails with | Notes |
|---|---|---|---|---|---|
IncrementalUpdateWriter::writeRevision | BinaryBuffer $buffer, ObjectRegistry $registry, int $prevXrefOffset, int $catalogObject, array $catalogEntries, array $catalogUpdates, array $newObjectNumbers, string $fileId | Static. Re-writes the catalog with merged entries, appends a traditional cross-reference table for new and modified objects, and writes a trailer with /Size, /Root, /Prev, and /ID. Verifies the pre-revision prefix is byte-equal afterward. | int — byte offset of the new cross-reference table | \NextPDF\Exception\WriterException when the append-only prefix check fails; getWriterState() returns dss-append-only-invariant | Static entry point. No usable output on violation. |
ObjectStreamWriter::addObject | int $objectNumber, string $content | Appends one object to the pending stream after a size check. | void | OverflowException when the combined index plus body would exceed 65,536 bytes | $content excludes the N 0 obj / endobj wrappers. |
ObjectStreamWriter::canAccept | string $content | Estimates index overhead and tests the running total against the maximum. | bool | Does not throw | Pure predicate; no state change. |
ObjectStreamWriter::build | none | Builds the index, concatenates bodies, compresses with FlateDecode, and wraps the /Type /ObjStm dictionary. | string — raw Object Stream content | ObjectStreamWriteException when no objects were added, or on a zlib compression failure | Caller assigns the object number and wraps the markers. |
ObjectStreamWriter::getEntries | none | Recomputes body-relative offsets for the accumulated objects. | list<ObjectStreamEntry> | Does not throw | Offsets are relative to the body section. |
ObjectStreamWriter::count | none | Reports the number of accumulated objects. | int | Does not throw | — |
ObjStmCompressor::__construct | int $maxStreamSize = 65536, int $maxObjectsPerStream = 200 | Stores the size and object-count limits used for grouping. | — | Does not throw | Defaults match the module’s Object Stream tuning. |
ObjStmCompressor::groupObjects | list<array{number: int, generation?: int, content: string}> $objects | Filters ineligible objects, then packs the rest into writers within the size and count limits. | list<ObjectStreamWriter> | Does not throw; ineligible objects are skipped | Non-zero generation objects fall through to normal serialization. |
ObjStmCompressor::isEligible | string $content, int $generation = 0 | Rejects stream objects, /Encrypt, /XRef, /Catalog, and any non-zero generation. | bool | Does not throw | /Type matching is whitespace- and #xx-escape-tolerant. |
ObjStmCompressor::writeToBuffer | list<ObjectStreamWriter> $streams, BinaryBuffer $buffer, ObjectRegistry $registry | Allocates a carrier object per stream, registers type-2 compressed entries, and writes each ObjStm block. | list<int> — carrier object numbers | Propagates ObjectStreamWriteException from build() on a rare compression failure | Run after non-eligible objects are written and before the cross-reference is emitted. |
ObjStmCompressor::estimateSavings | list<ObjectStreamWriter> $streams, int $originalSize | Builds each stream to measure compressed size against the original. | ObjStmCompressionResult | Propagates ObjectStreamWriteException from build() on a rare compression failure | Read-only measurement helper. |
ObjectStreamEntry::__construct | int $objectNumber, string $content, int $offset | Immutable record of one packed object and its body offset. | — | Does not throw | final readonly; public properties. |
ObjStmCompressionResult::__construct | int $originalObjectCount, int $streamCount, int $estimatedOriginalSize, int $estimatedCompressedSize | Immutable metrics container. | — | Does not throw | final readonly; public properties. |
ObjStmCompressionResult::savedBytes | none | Returns original minus compressed size. | int | Does not throw | May be negative when packing expanded the data. |
ObjStmCompressionResult::savedPercent | none | Returns the percentage reduction. | float | Does not throw | Returns 0.0 when the original size is zero. |
ObjStmCompressionResult::compressionRatio | none | Returns compressed size over original. | float | Does not throw | Returns 1.0 when the original size is zero. |
ObjectStreamWriteException | — | Signals an Object Stream build failure. | — | Extends RuntimeException | Thrown by build(); catchable via RuntimeException for backward compatibility. |
Entry-point signatures
Section titled “Entry-point signatures”final class IncrementalUpdateWriter{ public static function writeRevision( BinaryBuffer $buffer, ObjectRegistry $registry, int $prevXrefOffset, int $catalogObject, array $catalogEntries, array $catalogUpdates, array $newObjectNumbers, string $fileId, ): int;}final class ObjectStreamWriter{ public function addObject(int $objectNumber, string $content): void; public function canAccept(string $content): bool; public function build(): string; /** @return list<ObjectStreamEntry> */ public function getEntries(): array; public function count(): int;}final class ObjStmCompressor{ public function __construct( int $maxStreamSize = 65536, int $maxObjectsPerStream = 200, );
/** * @param list<array{number: int, generation?: int, content: string}> $objects * @return list<ObjectStreamWriter> */ public function groupObjects(array $objects): array;
public function isEligible(string $content, int $generation = 0): bool;
/** * @param list<ObjectStreamWriter> $streams * @return list<int> */ public function writeToBuffer(array $streams, BinaryBuffer $buffer, ObjectRegistry $registry): array;
/** @param list<ObjectStreamWriter> $streams */ public function estimateSavings(array $streams, int $originalSize): ObjStmCompressionResult;}Behavior contract
Section titled “Behavior contract”writeRevision writes one incremental-update revision. It snapshots the existing buffer prefix before writing. It re-writes the catalog with merged entries, registers new object offsets, writes a traditional cross-reference table grouped into contiguous subsections, and writes a trailer with /Size, /Root, /Prev, and /ID. After writing, it compares the prefix again. If any earlier byte changed, it raises WriterException carrying the append-only-violation state and returns no usable output. On success it returns the byte offset of the new cross-reference table for chaining further revisions. Mixing cross-reference tables and streams across revisions is permitted.
ObjectStreamWriter accumulates objects. addObject raises an overflow error when the combined index and body would exceed the maximum of 65,536 bytes uncompressed. build raises an error on an empty stream; otherwise it compresses the index plus body and returns the Object Stream content with the /Type /ObjStm, /N, /First, /Length, and /Filter /FlateDecode entries. The caller assigns the object number and wraps the N 0 obj / endobj markers.
ObjStmCompressor decides which objects to pack. It excludes stream objects, encryption dictionaries, cross-reference streams, the document catalog, and any object with a non-zero generation number. writeToBuffer allocates a carrier object per stream, registers each packed object as a type-2 compressed cross-reference entry, and writes the ObjStm block at the current buffer offset. estimateSavings builds each stream to compute size metrics without mutating the buffer.
Edge cases & failure modes
Section titled “Edge cases & failure modes”- The append-only check copies the existing prefix. Its cost grows with the size of the already-written document. This cost is intentional and protects signed bytes.
- The Object Stream limit applies to the uncompressed index plus body. Place the encryption dictionary and other excluded object types as direct indirect objects.
/Typeexclusion is tolerant of arbitrary inter-token whitespace and#xxhex escaping. Forms such as/Type /Encrypt,/Type\n/Encrypt, and/Type /#45ncryptare all rejected, not only the canonical literal spelling.- Any object carrying a non-zero generation number is treated as ineligible and falls through to normal
N G obj … endobjserialization, because a compressed object’s generation is implicitly zero. writeToBuffermust run after all non-eligible objects have been written and before the cross-reference is emitted. Packed objects must not also be serialized separately.
FIPS-mode behavior
Section titled “FIPS-mode behavior”The Writer module performs no cryptographic operations. It protects signed bytes by refusing to emit when an earlier byte would change, which is a byte-equality test rather than a cryptographic one. FIPS algorithm selection for signing and hashing is governed by the signing module, not by this writer. Enabling or disabling FIPS mode does not change any Writer method’s behavior.
Conformance
Section titled “Conformance”NextPDF implements the module against ISO 32000-2:2020. The incremental writer follows the §7.5.6 incremental-update grammar: each revision appends a cross-reference section covering only new, changed, or deleted objects, and a trailer whose /Prev entry gives the offset of the previous cross-reference. The Object Stream builder follows the §7.5.7 object-stream model: an index of object-number and offset pairs, with offsets measured from the /First entry in increasing order, precedes the packed object bodies. Both clause references were verified against the ISO 32000-2:2020 corpus. Revision chaining for PAdES B-LT and B-LTA workflows follows ETSI EN 319 142-1 §5.4, as annotated in the source.
Development notes
Section titled “Development notes”- Install the package with
composer require nextpdf/pro:^3. The classes resolve underNextPDF\Pro\Writer. IncrementalUpdateWriter::writeRevisionis a static entry point; it holds no instance state between revisions.ObjectStreamEntry,ObjStmCompressionResult,IncrementalUpdateWriter, and the compressor together form the module’s public surface; the repository ships no runnable example for it.- A
WriterExceptionfromwriteRevisionindicates an append-only violation. Treat it as a hard failure and discard the buffer. - Object Stream carriers are indirect objects; the caller assigns their object numbers through the registry.
Publication boundary
Section titled “Publication boundary”This page documents externally observable behavior and the supported public API surface only. Internal namespace paths, helper classes, mechanism tables, runbook filenames, and ticket prefixes are out of scope.