stability: Experimental
PageBackfill: retained page buffer
At a glance
Section titled “At a glance”Opt-in preview. The retained page buffer is default-off. With it off, the writer is the streaming serializer it has always been — byte-identical. Turn it on only when you genuinely need to draw onto an earlier page, and read the fail-closed list below first.
By default, the writer streams pages and flushes them in order; once a page is flushed it cannot be drawn on again. The retained page buffer is the opt-in that holds flushed pages so a previously flushed page can be back-filled — drawn onto an earlier page — before the document is serialized. The classic use is a total or a summary box that you can place only after later pages have been laid out.
Install
Section titled “Install”composer require nextpdf/core:^3The retained page buffer ships in the core package. Config::withRetainedPageBuffer()
and the Document back-fill methods are @since 6.1.0. The default remains the
streaming writer. ADR-037, which had previously deferred this capability, is now
recorded as implemented.
Conceptual overview
Section titled “Conceptual overview”Config::withRetainedPageBuffer() opts a document into retained pages. Once on,
Document::setActiveBackfillPage(int $pageIndex) redirects drawing to an earlier,
already-flushed page; Document::endPageBackfill() returns drawing to the normal
append position. Content you write between the two calls lands on the earlier
page. The buffer holds pages until save(), so the back-fill is applied before
the cross-reference table and trailer are written (ISO 32000-2 §7.5).
Fail-closed boundary — refused combinations
Section titled “Fail-closed boundary — refused combinations”Back-fill is a random-access operation, and several document features assume append-only, streamed bytes. The retained page buffer refuses to combine with any of them, order-independently and before serialization, so it can never silently break a signature or a conformance claim:
- A digital signature.
- Tagged PDF (structure tree).
- PDF/A.
- Linearization.
- Object-stream packing.
- Encryption.
- Safe CSS rendering mode.
A per-document uncompressed-bytes budget caps how much the buffer may hold; a document that exceeds it hard-fails rather than consuming unbounded memory. The streaming default still fails closed the moment a caller attempts a random-access switch without the opt-in — turning the buffer on is the only way to get back-fill, and it is incompatible with the features above by construction.
API surface
Section titled “API surface”| Symbol | Location | Role |
|---|---|---|
Config::withRetainedPageBuffer(bool $enabled = true): self | src/Core/Config.php | Opt a document into the retained page buffer. |
Document::setActiveBackfillPage(int $pageIndex): static | src/Core/Document.php | Redirect drawing to an earlier, already-flushed page. |
Document::endPageBackfill(): static | src/Core/Document.php | Return drawing to the normal append position. |
A back-fill attempt that violates a refused combination raises a typed configuration exception at the boundary, not a corrupted document.
Code sample — Quick start
Section titled “Code sample — Quick start”Reserve a spot on page one, fill the rest of the document, then back-fill the reserved spot with a value computed at the end.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;
$config = (new Config())->withRetainedPageBuffer();
$doc = Document::createStandalone($config);$doc->addPage(); // page 0 — leaves room for a grand total$doc->writeHtml('<h1>Invoice</h1>');
$doc->addPage(); // page 1 — line items$doc->writeHtml('<p>Line items…</p>');$total = 1234.56; // computed after laying out the items
$doc->setActiveBackfillPage(0); // draw back onto page 0$doc->writeHtml('<p>Grand total: ' . number_format($total, 2) . '</p>');$doc->endPageBackfill();
$doc->save(__DIR__ . '/invoice.pdf');Code sample — Production
Section titled “Code sample — Production”Keep the buffer off for any signed, tagged, PDF/A, linearized, encrypted, or object-stream document — those are exactly the combinations the buffer refuses. Choose one path explicitly.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Config;use NextPDF\Core\Document;
function renderReport(bool $needsBackfill, bool $mustBeSigned): Document{ if ($needsBackfill && $mustBeSigned) { // The buffer refuses to combine with signing. Resolve the requirement // before building: pre-compute the value, or sign a separate pass. throw new \LogicException('Back-fill and signing are mutually exclusive.'); }
$config = new Config(); if ($needsBackfill) { $config = $config->withRetainedPageBuffer(); }
return Document::createStandalone($config);}Edge cases & gotchas
Section titled “Edge cases & gotchas”- Off is byte-identical. With the buffer off, the writer streams as before.
- Mutually exclusive with signing, tagging, PDF/A, linearization, object streams, encryption, and Safe CSS mode. The refusal is order-independent and fires before serialization. Plan the document for one mode or the other.
- A bytes budget hard-fails. The retained buffer is bounded; a document that exceeds the uncompressed-bytes budget fails rather than growing without limit.
- Pair the calls. Every
setActiveBackfillPage()should be matched by anendPageBackfill()so later content appends normally. - The streaming default refuses random access. Without the opt-in, a random-access switch fails closed. The buffer is the only supported path.
Performance
Section titled “Performance”The retained page buffer trades memory for the back-fill capability: it holds
flushed pages until save(), bounded by the per-document uncompressed-bytes
budget. The streaming writer’s flat memory profile applies only with the buffer
off. The performance_budget (wall_ms: 1500, peak_mb: 128) reflects the
higher memory ceiling of the retained path.
Security notes
Section titled “Security notes”The retained page buffer does not widen the input surface; it changes when bytes are serialized, not what is ingested. Its refusal to combine with encryption and signing is a safety property: a back-fill can never alter signed or encrypted bytes after the fact, because the two cannot be enabled together. The bytes budget bounds memory against a hostile document.
Conformance
Section titled “Conformance”| Statement | Spec | Clause |
|---|---|---|
| The writer serializes the body, cross-reference structure, and trailer at save time. | ISO 32000-2 | §7.5 |
This is a preview capability. NextPDF refuses the back-fill buffer for signed, tagged, PDF/A, linearized, encrypted, and object-stream documents, so it makes no conformance claim for those profiles through this path. No standards text is reproduced.
Compat (TCPDF) adapter
Section titled “Compat (TCPDF) adapter”The TCPDF compatibility adapter exposes this capability as a constructor
extension. Construct the adapter with retainedPageBuffer: true, then a
setPage() or lastPage() call that targets an earlier page delegates to the
core back-fill instead of raising the streaming UnsupportedFeatureException.
This constructor argument is a NextPDF extension, not legacy TCPDF parity —
legacy TCPDF has no such flag. The same fail-closed refusals apply. See the
compat adapter’s retained-page-buffer page for the adapter-side details.