Skip to content
getnextpdf.com

Pro edition

Writer — Deep Reference

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.

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.

The module lives under the NextPDF\Pro\Writer namespace. All public symbols are listed below. Value objects are immutable final readonly classes.

SymbolParametersDefault behaviorReturnsThrows or fails withNotes
IncrementalUpdateWriter::writeRevisionBinaryBuffer $buffer, ObjectRegistry $registry, int $prevXrefOffset, int $catalogObject, array $catalogEntries, array $catalogUpdates, array $newObjectNumbers, string $fileIdStatic. 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-invariantStatic entry point. No usable output on violation.
ObjectStreamWriter::addObjectint $objectNumber, string $contentAppends one object to the pending stream after a size check.voidOverflowException when the combined index plus body would exceed 65,536 bytes$content excludes the N 0 obj / endobj wrappers.
ObjectStreamWriter::canAcceptstring $contentEstimates index overhead and tests the running total against the maximum.boolDoes not throwPure predicate; no state change.
ObjectStreamWriter::buildnoneBuilds the index, concatenates bodies, compresses with FlateDecode, and wraps the /Type /ObjStm dictionary.string — raw Object Stream contentObjectStreamWriteException when no objects were added, or on a zlib compression failureCaller assigns the object number and wraps the markers.
ObjectStreamWriter::getEntriesnoneRecomputes body-relative offsets for the accumulated objects.list<ObjectStreamEntry>Does not throwOffsets are relative to the body section.
ObjectStreamWriter::countnoneReports the number of accumulated objects.intDoes not throw
ObjStmCompressor::__constructint $maxStreamSize = 65536, int $maxObjectsPerStream = 200Stores the size and object-count limits used for grouping.Does not throwDefaults match the module’s Object Stream tuning.
ObjStmCompressor::groupObjectslist<array{number: int, generation?: int, content: string}> $objectsFilters ineligible objects, then packs the rest into writers within the size and count limits.list<ObjectStreamWriter>Does not throw; ineligible objects are skippedNon-zero generation objects fall through to normal serialization.
ObjStmCompressor::isEligiblestring $content, int $generation = 0Rejects stream objects, /Encrypt, /XRef, /Catalog, and any non-zero generation.boolDoes not throw/Type matching is whitespace- and #xx-escape-tolerant.
ObjStmCompressor::writeToBufferlist<ObjectStreamWriter> $streams, BinaryBuffer $buffer, ObjectRegistry $registryAllocates a carrier object per stream, registers type-2 compressed entries, and writes each ObjStm block.list<int> — carrier object numbersPropagates ObjectStreamWriteException from build() on a rare compression failureRun after non-eligible objects are written and before the cross-reference is emitted.
ObjStmCompressor::estimateSavingslist<ObjectStreamWriter> $streams, int $originalSizeBuilds each stream to measure compressed size against the original.ObjStmCompressionResultPropagates ObjectStreamWriteException from build() on a rare compression failureRead-only measurement helper.
ObjectStreamEntry::__constructint $objectNumber, string $content, int $offsetImmutable record of one packed object and its body offset.Does not throwfinal readonly; public properties.
ObjStmCompressionResult::__constructint $originalObjectCount, int $streamCount, int $estimatedOriginalSize, int $estimatedCompressedSizeImmutable metrics container.Does not throwfinal readonly; public properties.
ObjStmCompressionResult::savedBytesnoneReturns original minus compressed size.intDoes not throwMay be negative when packing expanded the data.
ObjStmCompressionResult::savedPercentnoneReturns the percentage reduction.floatDoes not throwReturns 0.0 when the original size is zero.
ObjStmCompressionResult::compressionRationoneReturns compressed size over original.floatDoes not throwReturns 1.0 when the original size is zero.
ObjectStreamWriteExceptionSignals an Object Stream build failure.Extends RuntimeExceptionThrown by build(); catchable via RuntimeException for backward compatibility.
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;
}

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.

  • 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.
  • /Type exclusion is tolerant of arbitrary inter-token whitespace and #xx hex escaping. Forms such as /Type /Encrypt, /Type\n/Encrypt, and /Type /#45ncrypt are 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 … endobj serialization, because a compressed object’s generation is implicitly zero.
  • writeToBuffer must run after all non-eligible objects have been written and before the cross-reference is emitted. Packed objects must not also be serialized separately.

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.

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.

  • Install the package with composer require nextpdf/pro:^3. The classes resolve under NextPDF\Pro\Writer.
  • IncrementalUpdateWriter::writeRevision is 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 WriterException from writeRevision indicates 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.

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.