Skip to content

Create optional content layers (OCG)

Wrap content in named optional content groups (OCGs), often called layers. A PDF reader can toggle each layer in its Layers panel, and one layer starts hidden by default. This recipe follows examples/26-layers.php.

An OCG is an ISO 32000-2 optional content group dictionary with Type /OCG. NextPDF wraps the layered marks between BDC/EMC and uses the OC marked-content tag.

Terminal window
composer require nextpdf/core:^3

You do not need an optional extension. The layer API has been stable since 1.0.0 and runs on the 8.1–8.4 backport matrix.

startLayer($name, $visible) opens an OCG. Everything you draw until the matching endLayer() belongs to that group. $name is the label the PDF reader shows in its Layers panel. ISO 32000-2 requires the OCG Name, which is the user-facing string. Passing $visible: false registers the group in the default configuration’s OFF state, so the reader hides it until the user turns it on.

Visibility is reader-cooperative. For an optional content membership dictionary (OCMD), the default visibility policy is AnyOn. The layer is visible if any referenced group is ON. A hidden layer is hidden only by reader convention. It is not removed or protected, and it is neither a redaction nor a security control. To remove content, do not draw it.

PHPDoc auto-generates the API surface. This recipe uses these two methods:

  • startLayer(string $name, bool $visible = true): static — opens a named OCG; $visible: false makes it hidden by default.
  • endLayer(): static — closes the most recently opened layer (balanced with startLayer()).
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$doc->startLayer('Content', visible: true);
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 8, 'Always-visible body content.', newLine: true);
$doc->endLayer();
$doc->startLayer('Debug Grid', visible: false); // hidden until toggled
$doc->setDrawColor(200, 200, 200);
for ($x = 0.0; $x <= 210.0; $x += 10.0) {
$doc->line($x, 0, $x, 297);
}
$doc->endLayer();
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/layers.pdf');

Use this complete, harness-ready example. It honors NEXTPDF_COOKBOOK_OUTPUT and introduces no entropy of its own.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Contracts\Alignment;
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Layer Examples (OCG)');
$doc->addPage();
// Layer 1 — background, visible by default.
$doc->startLayer('Background', visible: true);
$doc->setFillColor(230, 240, 250);
$doc->rect(10, 10, 190, 277, 'F');
$doc->endLayer();
// Layer 2 — watermark, visible by default; can be toggled off.
$doc->startLayer('Watermark', visible: true);
$doc->setFont('helvetica', 'B', 54);
$doc->setTextColor(200, 200, 200);
$doc->startTransform();
$doc->rotate(45, 105, 148);
$doc->setXY(30, 135);
$doc->cell(150, 20, 'DRAFT', align: Alignment::Center);
$doc->stopTransform();
$doc->endLayer();
// Layer 3 — main content, visible by default.
$doc->startLayer('Content', visible: true);
$doc->setTextColor(0);
$doc->setFont('helvetica', 'B', 20);
$doc->setXY(10, 15);
$doc->cell(0, 14, 'Layer Examples (OCG)', newLine: true);
$doc->ln(4);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'This document contains four optional content groups. '
. "Toggle them in your reader's Layers panel.");
$doc->endLayer();
// Layer 4 — debug grid, hidden by default.
$doc->startLayer('Debug Grid', visible: false);
$doc->setDrawColor(180, 180, 180);
$doc->setLineWidth(0.15);
for ($x = 0.0; $x <= 210.0; $x += 10.0) {
$doc->line($x, 0, $x, 297);
}
for ($y = 0.0; $y <= 297.0; $y += 10.0) {
$doc->line(0, $y, 210, $y);
}
$doc->endLayer();
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/layers.pdf';
$doc->save($out);
echo "Created layers.pdf\n";
  • Balance every startLayer() with an endLayer(). An unclosed layer leaves a dangling BDC with no EMC and makes the document structure malformed. Pair each opening call with its closing call.
  • A hidden layer is not removed. visible: false hides content only by reader convention. The marks and any text remain in the file and are recoverable. This is not redaction. For sensitive data, do not draw it.
  • Layer panel support varies. Toggling requires a reader that exposes optional content. Print pipelines and minimal viewers may always show, or always hide, default-off layers.
  • Nesting. Nested layers are allowed, but each inner group’s visibility remains independent. Do not assume an outer-OFF layer hides an inner-ON group unless you wire a membership policy.

Each layer adds one OCG dictionary and a BDC/EMC pair around its marks. The overhead is negligible. Cost scales with the content inside the layers, not the number of layers, so this stays well within the 2000 ms / 64 MB budget.

Optional content visibility is reader-cooperative, not access control. Hiding a layer does not encrypt, redact, or remove its content. Anyone can re-enable the layer or extract the bytes. Never use a hidden layer to conceal confidential text; omit the content entirely instead. This recipe parses no input and makes no network requests.

StatementSpecClausereference_id
An OCG dictionary has Type /OCG.ISO 32000-2§8.11.2
The OCG Name is the required user-facing label.ISO 32000-2§8.11.2
Optional content is enclosed between BDC/EMC with the OC tag.ISO 32000-2§8.11.3.2
OCMD policies are AllOn/AnyOn/AnyOff/AllOff (default AnyOn).ISO 32000-2§8.11.4.3

Reproducibility profile — structural. The trailer /ID and date atoms change on every save. The harness strips those atoms and compares the qpdf-normalised structure. This recipe describes how NextPDF produces that structure. It does not assert general ISO 32000-2 conformance.

Not applicable. Optional content groups are a Core capability with no Premium gate.