Ga naar inhoud

Een inhoudsopgave genereren vanuit de documentstructuur tijdens runtime

Uw inhoud kan tijdens runtime vorm krijgen: hoofdstukken uit een database, secties uit een Application Programming Interface (API)-respons of koppen uit een lus die u niet vooraf kunt kennen. U wilt dat de documentoverzichtsstructuur en de aanklikbare inhoudsopgave precies aansluiten op die inhoud, zonder een tweede, handmatig bijgehouden lijst die uit de pas kan gaan lopen.

Dit recipe bouwt de overzichtsstructuur dynamisch op. Telkens wanneer u een kop schrijft, leest u de actuele cursor- en paginapositie van de engine met getPage(), getY() en getNumPages(), en geeft u die waarden vervolgens door aan bookmark(). De bladwijzer wordt gebonden aan de positie die u op dat moment uitleest, zodat de overzichtsstructuur de inhoud volgt, zelfs wanneer pagina-einden op onverwachte plaatsen vallen. Aan het einde rendert addTOC() een echte inhoudsopgavepagina op basis van dezelfde vermeldingen.

Vereisten: een Core-installatie (composer require nextpdf/core:^3) en inhoud waarvan u de kopstructuur pas tijdens het schrijven ontdekt, niet vooraf.

Deze pagina behandelt het dynamische, positiegestuurde patroon. Lees voor het statische geval, waarin u elke kop en het bijbehorende niveau vooraf kent, eerst Bladwijzers en een inhoudsopgave toevoegen. Dit recipe gebruikt dezelfde bookmark()- en addTOC()-interface en herhaalt die basis niet.

Terminal window
composer require nextpdf/core:^3

U hebt geen optionele extensie nodig. De navigatie-interface (bookmark(), addTOC()) en de positie-accessors (getPage(), getY(), getNumPages()) zijn stabiel sinds 1.2.0 en werken op de volledige backport-matrix van 8.1 tot en met 8.4.

Een dynamische inhoudsopgave bestaat uit twee delen die op elkaar moeten aansluiten:

  • De overzichtsstructuur (ook wel bladwijzers genoemd): de boomstructuur die de lezer in de navigatiezijbalk ziet, waarbij elke vermelding naar een positie in het document springt.
  • De weergegeven inhoudsopgave: een gegenereerde pagina die dezelfde vermeldingen met hun paginanummers opsomt.

NextPDF houdt beide synchroon met één enkele aanroep. bookmark($title, $level, $y) voegt één overzichtsitem en één inhoudsopgavevermelding toe, beide gebonden aan de huidige pagina en de huidige verticale positie. U hoeft geen twee lijsten bij te houden.

Het dynamische deel is waar de positie vandaan komt. Een statisch recipe geeft vaste koppen door in de volgorde van de broncode. Hier schrijft u een kop en vraagt u de engine vervolgens meteen waar de cursor terechtkwam:

  • getPage() retourneert de nulgebaseerde index van de actieve pagina. Voordat de eerste pagina is toegevoegd, retourneert het -1.
  • getNumPages() retourneert het totale aantal pagina’s, inclusief de actieve pagina die nog niet is afgerond.
  • getY() retourneert de huidige verticale cursor in gebruikerseenheden, gemeten als de afstand vanaf de bovenkant van de pagina.
  • getX(), getPageHeight() en getMargins() geven de aanvullende positiegegevens wanneer u moet bepalen of een kop en de eerste regel van de bijbehorende bodytekst samen passen.

Lees die waarden uit en roep vervolgens bookmark() aan. Een automatisch pagina-einde kan de cursor tussen twee koppen naar een nieuwe pagina verplaatsen, dus door de positie terug te lezen blijft de overzichtsbestemming op de juiste pagina.

Eén punt in de volgorde bepaalt het hele patroon: roep bookmark() aan precies op het punt waar u de bestemming wilt, namelijk onmiddellijk voordat u de koptekst rendert. Als u eerst de kop schrijft en pas daarna een bladwijzer plaatst, ligt de vastgelegde getY() direct onder de kop.

Dit recipe maakt gebruik van deze methoden van \NextPDF\Core\Document:

  • bookmark(string $title, int $level = 0, float $y = -1): static - voegt op $level een overzichtsitem en een inhoudsopgavevermelding toe, gebonden aan de huidige pagina. Met $y = -1 is de bestemming de huidige cursor-Y; geef een niet-negatieve Y door om een precieze bestemming vast te pinnen.
  • addTOC(int $pageIndex = 0, string $title = ''): static - rendert een inhoudsopgavepagina op basis van de verzamelde vermeldingen en voegt deze in op $pageIndex. Keert terug zonder een pagina in te voegen wanneer er geen bladwijzers zijn.
  • getPage(): int - nulgebaseerde index van de actieve pagina (-1 vóór de eerste pagina).
  • getNumPages(): int - totale paginatelling, inclusief de actieve, nog niet doorgeschreven pagina.
  • getY(): float - huidige cursor-Y in gebruikerseenheden (afstand vanaf de bovenkant van de pagina).
  • getX(): float - huidige cursor-X in gebruikerseenheden.
  • getPageHeight(): float - hoogte van de huidige pagina in gebruikerseenheden.
  • getMargins(): \NextPDF\ValueObjects\Margin - de actieve marges (top, right, bottom, left).
  • setY(float $y): static - verplaatst de cursor naar een expliciete Y.
  • setAutoPageBreak(bool $enabled, float $margin = 20): static - beheert het automatische pagina-einde en de bijbehorende drempel voor de ondermarge.

Dit voorbeeld schrijft drie secties uit een runtime-lijst. Elke iteratie leest met getPage() de actuele pagina uit vóór het plaatsen van de bladwijzer, zodat de overzichtsbestemming correct blijft na een automatisch pagina-einde.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
/** @var list<array{title: string, body: string}> $sections */
$sections = [
['title' => 'Origins', 'body' => 'Runtime content for the first section.'],
['title' => 'Method', 'body' => 'Runtime content for the second section.'],
['title' => 'Results', 'body' => 'Runtime content for the third section.'],
];
$doc = Document::createStandalone();
$doc->addPage();
foreach ($sections as $section) {
// Read the live page back, then bookmark BEFORE rendering the heading,
// so the destination points at the heading, not below it.
$pageIndex = $doc->getPage();
$doc->bookmark($section['title'], level: 0);
$doc->setFont('helvetica', 'B', 16);
$doc->cell(0, 10, $section['title'], newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, $section['body']);
$doc->ln(6);
echo "Bookmarked '{$section['title']}' on page index {$pageIndex}\n";
}
// Splice the rendered table of contents in as the first page.
$doc->addTOC(pageIndex: 0, title: 'Contents');
$doc->save(getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/dynamic-toc.pdf');

Verwachte terminaluitvoer, met één regel per sectie:

Bookmarked 'Origins' on page index 0
Bookmarked 'Method' on page index 0
Bookmarked 'Results' on page index 0

Deze versie bouwt een overzichtsstructuur met twee niveaus (hoofdstukken en secties) op uit een geneste runtime-structuur. De kop blijft bij de eerste regel van de bijbehorende bodytekst doordat de positie wordt uitgelezen voordat er wordt geschreven, en de generatie wordt omhuld met try/catch-blokken voor de meest specifieke NextPDF-uitzonderingen. PageLayoutException dekt fouten tijdens de generatie af, zoals het overschrijden van het paginaplafond. save() genereert InvalidConfigException voor een niet-beschrijfbaar of onveilig uitvoerpad.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Exception\InvalidConfigException;
use NextPDF\Exception\PageLayoutException;
/**
* Render a report whose chapter and section structure is known only at runtime,
* building the outline and table of contents from the live cursor position.
*
* @param list<array{title: string, sections: list<array{title: string, body: string}>}> $chapters
*
* @throws PageLayoutException When page generation exceeds an engine limit.
* @throws InvalidConfigException When the output path cannot be written.
*/
function renderDynamicToc(array $chapters, string $outputPath): void
{
$doc = Document::createStandalone();
$doc->setTitle('Runtime Report');
$doc->setPrintHeader(false);
$doc->setPrintFooter(false);
// A 25 mm bottom threshold so a heading does not strand at the page foot.
$doc->setAutoPageBreak(true, margin: 25);
$doc->addPage();
foreach ($chapters as $chapter) {
// Reserve space so the chapter heading and its first section start
// together: if less than 40 user units remain, break first.
$remaining = $doc->getPageHeight() - $doc->getMargins()->bottom - $doc->getY();
if ($remaining < 40.0) {
$doc->addPage();
}
// Bookmark at the destination point, before the heading is drawn.
$doc->bookmark($chapter['title'], level: 0);
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 12, $chapter['title'], newLine: true);
$doc->ln(3);
foreach ($chapter['sections'] as $section) {
$doc->bookmark($section['title'], level: 1);
$doc->setFont('helvetica', 'B', 13);
$doc->cell(0, 9, $section['title'], newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, $section['body']);
$doc->ln(5);
}
}
// Render the table of contents only when at least one bookmark exists.
// addTOC() is a no-op when the entry list is empty, so an empty report
// produces no contents page rather than a blank one.
$doc->addTOC(pageIndex: 0, title: 'Table of Contents');
$doc->save($outputPath);
}
/** @var list<array{title: string, sections: list<array{title: string, body: string}>}> $chapters */
$chapters = [
[
'title' => 'Chapter 1: Overview',
'sections' => [
['title' => 'Scope', 'body' => 'Runtime body text for the scope section.'],
['title' => 'Audience', 'body' => 'Runtime body text for the audience section.'],
],
],
[
'title' => 'Chapter 2: Detail',
'sections' => [
['title' => 'Inputs', 'body' => 'Runtime body text for the inputs section.'],
],
],
];
$output = getenv('NEXTPDF_COOKBOOK_OUTPUT') ?: __DIR__ . '/dynamic-toc.pdf';
try {
renderDynamicToc($chapters, $output);
echo "Wrote {$output}\n";
} catch (PageLayoutException $e) {
// A structural limit was hit during generation; surface the page context.
fwrite(STDERR, 'Layout failure while building the report: ' . $e->getMessage() . "\n");
exit(1);
} catch (InvalidConfigException $e) {
// The output path was rejected (stream wrapper, missing directory, or
// a null byte). Report it without leaking the resolved path to a client.
fwrite(STDERR, 'Output path rejected: ' . $e->getMessage() . "\n");
exit(1);
}
  • getPage() retourneert -1 vóór de eerste pagina. Voeg de eerste pagina toe voordat u de positie uitleest of bookmark() aanroept. De voorbeelden voegen vooraf een pagina toe.
  • Plaats de bladwijzer vóór de kop, niet erna. bookmark() met $y = -1 legt de huidige getY() vast. Roep deze methode onmiddellijk aan voordat u de kop rendert, zodat de bestemming op de kop terechtkomt en niet op de regel eronder.
  • Automatische pagina-einden verplaatsen de bestemming. Wanneer setAutoPageBreak() aanstaat, kan een aanroep van cell() of multiCell() op een nieuwe pagina terechtkomen. Lees getPage() bij de volgende iteratie opnieuw uit in plaats van het in de cache te bewaren. De bestemming volgt de inhoud doordat bookmark() telkens de actuele positie uitleest.
  • Reserveer ruimte voor een kop en de bijbehorende eerste regel samen. Een kop die onder aan de pagina past terwijl de bijbehorende bodytekst naar de volgende pagina doorloopt, leest slecht. Het productievoorbeeld berekent de resterende hoogte uit getPageHeight(), getMargins()->bottom en getY(), en forceert vervolgens een vroegtijdige addPage() wanneer er minder dan een drempelwaarde overblijft.
  • addTOC() doet niets bij een leeg document. Als er geen bookmark()-aanroep is uitgevoerd, keert addTOC() terug zonder een pagina in te voegen. Het beveiligen van het rapport tegen lege invoer is daarom niet vereist, al is het goed om te weten dat de inhoudsopgavepagina dan niet verschijnt.
  • De inhoudsopgave wordt eenmaal gerenderd, op de positie waar u deze invoegt. addTOC(pageIndex: 0) voegt de inhoudsopgave in als de eerste pagina. Paginanummers in de weergegeven vermeldingen gebruiken de vastgelegde pagina van elke vermelding, dus voeg de inhoudsopgave pas in nadat elke bookmark()-aanroep is uitgevoerd.
  • Het overslaan van niveaus oogt onjuist genest. Verhoog $level met hooguit één tussen opeenvolgende bladwijzers. Een sprong van niveau 0 naar niveau 2 zonder een tussenliggend niveau 1 levert een hiërarchie op die sommige readers onjuist weergeven.

Elke aanroep van bookmark() voegt één overzichtsitem en één inhoudsopgavevermelding toe in O(1)-tijd, en elke positielezing (getPage(), getY(), getNumPages()) is een veldtoegang met constante tijd op de renderingcontext, zonder traversal. De overzichtsboom en de inhoudsopgavepagina worden elk eenmaal gematerialiseerd: respectievelijk bij addTOC() en bij save(). Een rapport met honderden koppen blijft ruim binnen een budget van 2000 ms / 64 MB. De generatie verloopt in-process, zonder headless browser en zonder netwerkaanroep.

Bladwijzertitels en de inhoudsopgavepagina geven de waarden weer die u doorgeeft aan bookmark(). Wanneer die titels runtime-gegevens bevatten, zoals een hoofdstuknaam uit een databaserij of een API-veld, begrens dan de lengte en saneer de tekenreeks voordat deze bookmark() bereikt, precies zoals u dat zou doen met elke waarde die in de reader wordt weergegeven. Bouw geen titels op uit niet-gevalideerde requestinvoer.

De engine valideert het uitvoerpad dat aan save() wordt doorgegeven: het weigert stream-wrappers (scheme://) en ingebedde null-bytes, en het resolveert de bovenliggende map om path traversal te blokkeren, waarbij het InvalidConfigException genereert bij elk van deze voorwaarden. Houd die validatie effectief door een pad door te geven dat u beheert; geef save() nooit een onbewerkte, door de client aangeleverde bestandsnaam. Wanneer u een InvalidConfigException aan een aanroeper rapporteert, log dan de details aan de serverzijde en retourneer een algemene melding in plaats van het geresolveerde pad.

Dit recipe doet geen eigen ISO 32000-2-conformiteitsclaim. De semantiek van de overzichtsstructuur en de inhoudsopgave, waaronder de documentoverzichtsstructuur als een boom van overzichtsitems en de bij die items behorende bestemmingen, wordt beschreven in Bladwijzers en een inhoudsopgave toevoegen, waarin de relevante clausuleverwijzingen staan. Het dynamische patroon hier verandert alleen waar de bestemmingspositie vandaan komt, niet de structuur die wordt geschreven.

Reproduceerbaarheidsprofiel - structureel. De trailer-/ID en datumatomen variëren per save; een structurele vergelijking verwijdert die waarden. Deze pagina documenteert hoe NextPDF de overzichtsstructuur en inhoudsopgave uit de actuele cursor produceert; deze pagina doet geen algemene claim over conformiteit met de standaarden.