跳转到内容

放置内容前先检查版面边界框

在放置内容前,先读取文档当前的版面几何数据。本 recipe(范例)会查询页面边界框、边距、所有禁写区域以及当前光标位置。这样,你的放置逻辑就能依据真实数值运行,而不是依赖猜测坐标。本范例由 examples/34-inspect-layout-boxes.php 及其 tests/Cookbook/Php/InspectLayoutBoxesRecipeTest.php 测试载具支撑。

Terminal window
composer require nextpdf/core:^3

你不需要 Pro 或 Enterprise 软件包。版面查询接口属于 Core 的一部分,可在 PHP 8.1 到 8.4 上运行。

PDF 页面具有边界框。最基本的是 MediaBox,也就是定义页面范围的矩形(ISO 32000-2 §7.7.3.3)。内容在用户空间(user space)中定位。默认情况下,用户空间的原点位于左下角,一个单位等于 1/72 英寸(§8.3.2)。NextPDF 提供对作者友好的左上角视角,并通过只读查询方法公开这些几何数据:

  • getPageWidth() / getPageHeight() — 页面边界框的尺寸。
  • getMargins() — 当前生效的 Margin 值对象(上/右/下/左)。
  • getPageRegions() — 已声明的禁写区域(PageRegion[])。每个区域都是不可变矩形,禁止在其中放置内容。
  • getX() / getY() — 作者空间中的当前光标。

这些都是 幂等读取。它们不会输出内容、推进光标或改变状态。你可以用它们计算剩余垂直空间,再决定是继续写入,还是调用 addPage()。也可以用它们让某个区块相对于禁写区域排版,而不是写死偏移量。

API 接口由 PHPDoc 生成。主要入口点位于 \NextPDF\Core\Concerns\HasPagesHasLayout 这两个 trait 中:

  • Document::getPageWidth(): float / Document::getPageHeight(): float
  • Document::getMargins(): \NextPDF\ValueObjects\Margin
  • Document::getPageRegions(): arraylist<\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);

这是可独立运行、能在测试载具上执行的程序。它对应 范例 examples/34-inspect-layout-boxes.php。它会读取页面几何数据、声明一个页脚禁写区域,然后针对每个区块作出数据驱动的决策。如果下一个区块会与某个区域相撞,或超出下边距,它就会新增一页,而不是叠印到现有内容上。

<?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);

预期的 STDOUT 如下。页面边界框的数字反映默认的 A4 页面大小,光标则从左上角的内容原点开始:

Page box: 595.28 x 841.89 pt
Cursor start: (<x>, <y>)
Pages used: 2 (page breaks decided from layout geometry)
  • 在页面存在之前读取。 getPageWidth()getPageHeight() 反映的是 当前 这一页,所以要在 addPage() 之后再调用它们。在第一页出现之前,它们返回的是默认页面大小的几何数据,而不是你尚未新增的那一页。
  • 区域是作者空间中的矩形。 PageRegiony 表示从左上角起算的作者空间距离,与 getY() 一致。不要把它和原始的左下角 PDF 坐标混用。
  • 读取没有副作用。 这些查询方法都不会推进光标或输出内容。在密集循环中调用它们是安全的,成本也很低。
  • 边距可能逐页变动。 如果后续某一页设置了不同的边距,请重新读取 getMargins(),而不是缓存第一次的值。
  • 区域不会自动重排文字。 禁写区域是 你必须遵守的约束,而不是自动的文字换行边界。本范例展示了明确的碰撞检查。对于自由定位的写入,引擎绝不会自行把内容移出某个区域。

这里的每个方法都只是一次属性读取。它以常数时间运行,不分配内存,也没有任何输入或输出。本范例构建的小型多页文档远低于 1500 ms / 96 MB 的预算。可复现性配置文件属于 语义(semantic) 层级,因为生成的 PDF 带有 trailer 的 /ID 与创建时的元数据。本范例真正重要的是可观察的几何决策,因此测试载具会通过结构性的抽象语法树(AST)和元数据比对来验证这些决策,而不是使用字节哈希。

  • 数据驻留与 PII 缓解。 不适用。本范例读取几何数据,并排版调用端提供的文字,因此不会引入任何新的数据路径。请对你放置的文字应用与其他位置相同的最小化原则。
  • 安全遥测与日志清理。 本范例会打印几何数字和一行固定的进度消息。它不会记录文档文字。
  • 威胁模型。 不适用。这个查询接口是只读的,也不解析任何外部输入。它不是信任边界。
  • FIPS 模式行为。 不适用。不会执行任何密码学运算。
陈述规格条款参考 ID
页面对象通过边界框(MediaBox)定义其范围。ISO 32000-2§7.7.3.3
页面矩形就是内容边界。ISO 32000-2§7.7.3.3
位置以用户空间度量;一个单位是 1/72 英寸。ISO 32000-2§8.3.2

本 recipe 读取的几何数据,由所引用的 ISO 32000-2 页面边界框与用户空间条款定义。它并未主张全面符合 ISO 32000-2。所引用的条款正是版面查询接口所依赖的条款。