Перейти к содержимому

Проверка боксов макета перед размещением содержимого

Считывайте текущую геометрию макета документа перед размещением содержимого. Этот рецепт считывает бокс страницы, поля, области, запретные для записи, и текущее положение курсора. Так логика размещения сможет опираться на реальные числа, а не на угаданные координаты. Рецепт основан на examples/34-inspect-layout-boxes.php и его обвязке tests/Cookbook/Php/InspectLayoutBoxesRecipeTest.php.

Окно терминала
composer require nextpdf/core:^3

Пакет Pro или Enterprise не требуется. Интерфейс запросов к макету входит в Core и работает с PHP 8.1–8.4.

Страница в формате Portable Document Format (PDF) имеет граничные боксы. Базовый из них — MediaBox, прямоугольник, задающий размеры страницы (ISO 32000-2 §7.7.3.3). Содержимое размещается в пользовательском пространстве. По умолчанию пользовательское пространство начинается в левом нижнем углу, а одна единица равна 1/72 дюйма (§8.3.2). NextPDF предоставляет удобную для автора систему координат с началом в левом верхнем углу и открывает доступ к геометрии через методы запроса, доступные только для чтения:

  • getPageWidth() / getPageHeight() — размеры бокса страницы.
  • getMargins() — активный объект-значение Margin (top/right/bottom/left).
  • getPageRegions() — объявленные области, запретные для записи (PageRegion[]). Каждая из них — неизменяемый прямоугольник, в котором размещение содержимого запрещено.
  • getX() / getY() — текущее положение курсора в авторском пространстве.

Это идемпотентные операции чтения. Они не выводят содержимое, не перемещают курсор и не меняют состояние. Используйте их, чтобы вычислить оставшееся вертикальное пространство, а затем решить, продолжать запись или вызвать addPage(). Вы также можете располагать блок относительно области, запретной для записи, вместо жёстко заданного смещения.

Поверхность API генерируется из PHPDoc. Основные точки входа находятся в трейтах \NextPDF\Core\Concerns\HasPages и HasLayout:

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

Эту самодостаточную программу запускает обвязка. Она повторяет 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 мс / 96 МБ. Профиль воспроизводимости — семантический, потому что сгенерированный PDF несёт в трейлере /ID и метаданные создания. Главное — наблюдаемые решения по геометрии в примере, поэтому обвязка проверяет их, сравнивая структурное абстрактное синтаксическое дерево (AST) и метаданные, а не байтовый хеш.

  • Локализация данных и меры защиты персональных данных (PII). Неприменимо. Рецепт считывает геометрию и компонует текст, переданный вызывающей стороной, поэтому не добавляет нового пути обработки данных. Применяйте к размещаемому тексту ту же минимизацию, что и везде.
  • Безопасная телеметрия и очистка журналов. Пример печатает числа геометрии и фиксированную строку прогресса. Он не записывает в журнал текст документа.
  • Модель угроз. Неприменимо. Интерфейс запросов доступен только для чтения и не разбирает внешний ввод. Он не является границей доверия.
  • Поведение в режиме FIPS. Неприменимо. Криптографические операции не выполняются.
УтверждениеСпецификацияПунктИдентификатор ссылки (reference_id)
Объект страницы задаёт свои размеры через граничные боксы (MediaBox).ISO 32000-2§7.7.3.3
Прямоугольник страницы является границей содержимого.ISO 32000-2§7.7.3.3
Позиции измеряются в пользовательском пространстве; одна единица равна 1/72 дюйма.ISO 32000-2§8.3.2

Этот рецепт считывает геометрию, определённую процитированными пунктами ISO 32000-2 для бокса страницы и пользовательского пространства. Он не заявляет о полном соответствии ISO 32000-2. Интерфейс запросов к макету опирается на процитированные пункты.