Skip to content

Inspect layout boxes before placing content

Read a document’s live layout geometry before you place content. This recipe reads the page box, margins, no-write regions, and current cursor. Your placement logic can then use real numbers instead of guessed coordinates. The recipe is backed by examples/34-inspect-layout-boxes.php and its tests/Cookbook/Php/InspectLayoutBoxesRecipeTest.php harness.

Terminal window
composer require nextpdf/core:^3

You do not need a Pro or Enterprise package. The layout query surface is part of Core and runs on PHP 8.1 through 8.4.

A Portable Document Format (PDF) page has boundary boxes. The minimum is the MediaBox, the rectangle that defines the page extent (ISO 32000-2 §7.7.3.3). Content is positioned in user space. By default, user space starts at the lower-left corner, and one unit equals 1/72 inch (§8.3.2). NextPDF gives you an author-friendly top-left view and exposes the geometry through read-only query methods:

  • getPageWidth() / getPageHeight() — page box dimensions.
  • getMargins() — the active Margin value object (top/right/bottom/left).
  • getPageRegions() — the declared no-write zones (PageRegion[]). Each is an immutable rectangle where content placement is prohibited.
  • getX() / getY() — the live cursor in author space.

These are idempotent reads. They do not emit content, advance the cursor, or change state. Use them to compute the remaining vertical space, then decide whether to keep writing or call addPage(). You can also lay out a block relative to a no-write region instead of using a hard-coded offset.

The application programming interface (API) surface is generated from PHPDoc. The main entry points live in the \NextPDF\Core\Concerns\HasPages and HasLayout traits:

  • Document::getPageWidth(): float / Document::getPageHeight(): float
  • Document::getMargins(): \NextPDF\ValueObjects\Margin
  • Document::getPageRegions(): array (list<\NextPDF\Layout\PageRegion>)
  • Document::addPageRegion(float $x, float $y, float $w, float $h): static
  • Document::getX(): float / Document::getY(): float
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$pageHeight = $doc->getPageHeight();
$margins = $doc->getMargins();
$cursorY = $doc->getY();
// Vertical space left before the bottom margin.
$remaining = $pageHeight - $margins->bottom - $cursorY;
// Geometry is in user-space units (PDF points; 1 pt = 1/72 in).
echo sprintf("Page height: %.2f pt\n", $pageHeight);
echo sprintf("Bottom margin: %.2f pt\n", $margins->bottom);
echo sprintf("Cursor Y: %.2f pt\n", $cursorY);
echo sprintf("Remaining: %.2f pt\n", $remaining);

This self-contained program runs under the harness. It mirrors examples/34-inspect-layout-boxes.php. It reads the page geometry, declares a footer no-write region, and makes a data-driven decision for each block. If the next block would collide with a region or run past the bottom margin, it adds a page instead of overprinting.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->setTitle('Layout Box Inspection Demo');
$doc->setLanguage('en');
$doc->addPage();
// Read the geometry (idempotent, no side effects). Values are in
// user-space units (PDF points; 1 pt = 1/72 in).
$pageWidth = $doc->getPageWidth();
$pageHeight = $doc->getPageHeight();
$margins = $doc->getMargins();
echo sprintf("Page box: %.2f x %.2f pt\n", $pageWidth, $pageHeight);
echo sprintf("Cursor start: (%.2f, %.2f)\n", $doc->getX(), $doc->getY());
// A 15 pt tall footer no-write zone across the text column.
$footerHeight = 15.0;
$footerTop = $pageHeight - $margins->bottom - $footerHeight;
$doc->addPageRegion(
$margins->left,
$footerTop,
$pageWidth - $margins->left - $margins->right,
$footerHeight,
);
$blocks = [
'Section 1. Each block measures the live cursor against the page box '
. 'and any no-write region before it is placed.',
'Section 2. The lowest Y at which any region starts is the hard floor '
. 'for new content; crossing it forces a page break.',
'Section 3. Reading geometry is idempotent — the query methods never '
. 'advance the cursor, so they are safe in the placement loop.',
'Section 4. This final block forces a second page when the column is '
. 'already near the footer keep-out zone.',
];
$blockHeight = 42.0;
$pagesUsed = 1;
foreach ($blocks as $index => $text) {
$cursorY = $doc->getY();
// Lowest Y a region starts at — the hard floor for new content.
$regionFloor = $pageHeight - $margins->bottom;
foreach ($doc->getPageRegions() as $region) {
$regionFloor = min($regionFloor, $region->y);
}
if ($cursorY + $blockHeight > $regionFloor) {
$doc->addPage();
++$pagesUsed;
// A fresh page resets the region set; re-declare the footer zone.
$footerTop = $doc->getPageHeight() - $doc->getMargins()->bottom - $footerHeight;
$doc->addPageRegion(
$doc->getMargins()->left,
$footerTop,
$doc->getPageWidth() - $doc->getMargins()->left - $doc->getMargins()->right,
$footerHeight,
);
}
$doc->setFont('helvetica', 'B', 12);
$doc->cell(0, 8, 'Block ' . ($index + 1), newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, $text);
$doc->ln(6);
}
// The harness sets NEXTPDF_COOKBOOK_OUTPUT and runs this script under the
// semantic profile (validated by structural AST + metadata, not a byte hash).
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$doc->save($out !== false && $out !== '' ? $out : __DIR__ . '/inspect-layout-boxes.pdf');
echo sprintf("Pages used: %d (page breaks decided from layout geometry)\n", $pagesUsed);

Expected standard output (STDOUT). The page-box numbers reflect the default A4 page size, and the cursor starts at the top-left content origin:

Page box: 595.28 x 841.89 pt
Cursor start: (<x>, <y>)
Pages used: 2 (page breaks decided from layout geometry)
  • Read before the page exists. getPageWidth() and getPageHeight() reflect the current page, so call them after addPage(). Before the first page, they return the default page-size geometry, not a page you have not added yet.
  • Regions are author-space rectangles. A PageRegiony is the top-left author distance, consistent with getY(). Do not mix it with raw lower-left PDF coordinates.
  • Reads are side-effect free. None of the query methods advance the cursor or emit content. Calling them in a tight loop is safe and costs little.
  • Margins can change per page. If a later page sets different margins, re-read getMargins() instead of caching the first value.
  • Regions do not auto-reflow text. A no-write region is a constraint you must honor, not an automatic text-wrap boundary. The recipe shows the explicit collision check. For free-positioned writes, the engine never moves content out of a region on its own.

Every method here is a property read. It runs in constant time, with no allocation and no input or output. The example builds a small multi-page document well within the 1500 ms / 96 MB budget. The reproducibility profile is semantic, because the generated PDF carries the trailer /ID and creation metadata. The example’s observable geometry decisions matter most, so the harness validates those decisions through a structural abstract syntax tree (AST) plus metadata comparison rather than a byte hash.

  • Data Residency & PII Mitigations. Not applicable. The recipe reads geometry and lays out caller-supplied text, so it introduces no new data path. Apply the same minimization to the text you place as you would anywhere else.
  • Safe Telemetry & Log Scrubbing. The example prints geometry numbers and a fixed progress line. It does not log document text.
  • Threat model. Not applicable. The query surface is read-only and parses no external input. It is not a trust boundary.
  • FIPS-mode behavior. Not applicable. No cryptographic operation runs.
StatementSpecClausereference_id
A page object defines its extent through boundary boxes (MediaBox).ISO 32000-2§7.7.3.3
The page rectangle is the content boundary.ISO 32000-2§7.7.3.3
Positions are measured in user space; one unit is 1/72 inch.ISO 32000-2§8.3.2

This recipe reads the geometry defined by the cited ISO 32000-2 page-box and user-space clauses. It does not assert blanket ISO 32000-2 conformance. The layout query surface relies on the cited clauses.