跳到內容

放置內容前先檢視版面邊界框

在放置內容前,先讀取文件的即時版面幾何資料。這個 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。所引用的條款正是版面查詢介面所依賴的那些。