Generowanie spisu treści na podstawie struktury dokumentu w czasie wykonywania
W skrócie
Dział zatytułowany „W skrócie”Treść może kształtować się dopiero w czasie wykonywania: rozdziały z bazy danych, sekcje z odpowiedzi API albo nagłówki z pętli, których nie da się znać z wyprzedzeniem. Potrzebujesz konspektu dokumentu i klikalnego spisu treści, które dokładnie odpowiadają tej treści, bez utrzymywania drugiej, ręcznie prowadzonej listy, która mogłaby się rozsynchronizować.
Ten przepis buduje konspekt dynamicznie. Podczas zapisywania każdego nagłówka odczytujesz z silnika bieżącą stronę i pozycję kursora za pomocą getPage(), getY() i getNumPages(), a następnie przekazujesz te wartości do bookmark(). Zakładka zostaje powiązana z pozycją odczytaną w tym momencie, dzięki czemu konspekt podąża za treścią nawet wtedy, gdy podziały stron wypadają w nieoczekiwanych miejscach. Na końcu addTOC() renderuje właściwą stronę spisu treści z tych samych wpisów.
Wymagania wstępne: instalacja rdzenia Core (composer require nextpdf/core:^3) oraz treść, której strukturę nagłówków odkrywasz podczas generowania, a nie wcześniej.
Ta strona omawia dynamiczny wzorzec sterowany pozycją. W przypadku statycznym, gdy z góry znasz każdy nagłówek i jego poziom, przeczytaj najpierw Dodawanie zakładek i spisu treści. Ten przepis korzysta z tej samej powierzchni API bookmark() i addTOC() i nie powtarza tych podstaw.
Instalacja
Dział zatytułowany „Instalacja”composer require nextpdf/core:^3Nie potrzebujesz żadnego opcjonalnego rozszerzenia. Powierzchnia nawigacji (bookmark(), addTOC()) oraz metody dostępu do pozycji (getPage(), getY(), getNumPages()) są stabilne od wersji 1.2.0 i działają w całej macierzy backportów od 8.1 do 8.4.
Przegląd koncepcyjny
Dział zatytułowany „Przegląd koncepcyjny”Dynamiczny spis treści ma dwie części, które muszą pozostawać zgodne:
- Konspekt (zwany także zakładkami): drzewo widoczne dla czytelnika w bocznym panelu nawigacji, gdzie każdy wpis prowadzi do określonej pozycji w dokumencie.
- Wyrenderowany spis treści: wygenerowana strona z tymi samymi wpisami i ich numerami stron.
NextPDF utrzymuje oba elementy w synchronizacji za pomocą jednego wywołania. bookmark($title, $level, $y) dodaje jeden element konspektu oraz jeden wpis spisu treści, oba powiązane z bieżącą stroną i bieżącą pozycją pionową. Nie utrzymujesz dwóch list.
Element dynamiczny polega na tym, skąd pochodzi pozycja. W przepisie statycznym przekazujesz jawne nagłówki w kolejności źródłowej. Tutaj zapisujesz nagłówek, a następnie natychmiast pytasz silnik, gdzie znalazł się kursor:
getPage()zwraca liczony od zera indeks aktywnej strony. Zanim zostanie dodana pierwsza strona, zwraca-1.getNumPages()zwraca całkowitą liczbę stron, w tym aktywną stronę, która nie została jeszcze wypisana.getY()zwraca bieżącą pionową pozycję kursora w jednostkach użytkownika, mierzoną jako odległość od górnej krawędzi strony.getX(),getPageHeight()igetMargins()uzupełniają zestaw danych, gdy musisz zdecydować, czy nagłówek i pierwszy wiersz jego tekstu głównego mieszczą się razem.
Odczytaj te wartości, a następnie wywołaj bookmark(). Automatyczny podział strony może przenieść kursor na nową stronę między dwoma nagłówkami, więc ponowne odczytanie pozycji utrzymuje cel konspektu na właściwej stronie.
Cały wzorzec sprowadza się do jednej zasady kolejności: wywołaj bookmark() dokładnie w punkcie, w którym chcesz umieścić cel, czyli bezpośrednio przed wyrenderowaniem tekstu nagłówka. Jeśli najpierw zapiszesz nagłówek, a zakładkę utworzysz później, zapisana wartość getY() znajdzie się bezpośrednio pod nagłówkiem.
Powierzchnia API
Dział zatytułowany „Powierzchnia API”Ten przepis opiera się na następujących metodach \NextPDF\Core\Document:
bookmark(string $title, int $level = 0, float $y = -1): static- dodaje element konspektu i wpis spisu treści na poziomie$level, powiązany z bieżącą stroną. Przy$y = -1celem jest bieżąca wartość Y kursora; przekaż nieujemne Y, aby przypiąć dokładny cel.addTOC(int $pageIndex = 0, string $title = ''): static- renderuje stronę spisu treści z zebranych wpisów i wstawia ją w pozycji$pageIndex. Wraca bez wstawiania strony, gdy nie istnieje żadna zakładka.getPage(): int- liczony od zera indeks aktywnej strony (-1przed pierwszą stroną).getNumPages(): int- całkowita liczba stron, w tym aktywna, jeszcze niewypisana strona.getY(): float- bieżąca wartość Y kursora w jednostkach użytkownika (odległość od górnej krawędzi strony).getX(): float- bieżąca wartość X kursora w jednostkach użytkownika.getPageHeight(): float- wysokość bieżącej strony w jednostkach użytkownika.getMargins(): \NextPDF\ValueObjects\Margin- aktywne marginesy (top,right,bottom,left).setY(float $y): static- przesuwa kursor do jawnej wartości Y.setAutoPageBreak(bool $enabled, float $margin = 20): static- steruje automatycznym podziałem strony i jego progiem dolnego marginesu.
Przykład kodu — szybki start
Dział zatytułowany „Przykład kodu — szybki start”Ten przykład zapisuje trzy sekcje z listy generowanej w czasie wykonywania. W każdej iteracji odczytuje bieżącą stronę za pomocą getPage() przed utworzeniem zakładki, dzięki czemu cel konspektu pozostaje poprawny po automatycznym podziale strony.
<?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');Oczekiwane dane wyjściowe w terminalu, po jednym wierszu na sekcję:
Bookmarked 'Origins' on page index 0Bookmarked 'Method' on page index 0Bookmarked 'Results' on page index 0Przykład kodu — produkcja
Dział zatytułowany „Przykład kodu — produkcja”Ta wersja tworzy dwupoziomowy konspekt (rozdziały i sekcje) z zagnieżdżonej struktury generowanej w czasie wykonywania. Utrzymuje nagłówek razem z pierwszym wierszem tekstu głównego, odczytując pozycję przed renderowaniem, a generowanie opakowuje w bloki try/catch dla najbardziej szczegółowych wyjątków NextPDF. PageLayoutException obejmuje błąd generowania, taki jak przekroczenie limitu stron. save() zgłasza InvalidConfigException dla niezapisywalnej lub niebezpiecznej ścieżki wyjściowej.
<?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);}Przypadki brzegowe i pułapki
Dział zatytułowany „Przypadki brzegowe i pułapki”getPage()zwraca-1przed pierwszą stroną. Dodaj pierwszą stronę przed odczytaniem pozycji lub wywołaniembookmark(). Przykłady dodają stronę na początku.- Twórz zakładkę przed nagłówkiem, a nie po nim.
bookmark()z$y = -1zapisuje bieżącą wartośćgetY(). Wywołaj ją bezpośrednio przed wyrenderowaniem nagłówka, aby cel trafił na nagłówek, a nie na wiersz pod nim. - Automatyczne podziały stron przenoszą cel. Gdy
setAutoPageBreak()jest włączone, wywołaniecell()lubmultiCell()może umieścić treść na nowej stronie. OdczytajgetPage()ponownie w następnej iteracji, zamiast buforować tę wartość. Cel podąża za treścią, ponieważbookmark()za każdym razem odczytuje bieżącą pozycję. - Zarezerwuj miejsce na nagłówek i jego pierwszy wiersz razem. Nagłówek umieszczony tuż nad stopką, gdy jego treść przechodzi na następną stronę, źle się czyta. Przykład produkcyjny oblicza pozostałą wysokość na podstawie
getPageHeight(),getMargins()->bottomigetY(), a następnie wymusza wcześniejszeaddPage(), gdy pozostaje mniej niż zadany próg. addTOC()na pustym dokumencie nic nie robi. Jeśli nie wykonano żadnego wywołaniabookmark(),addTOC()zwraca bez wstawiania strony. Obsługa pustego wejścia w raporcie nie jest zatem wymagana, choć warto wiedzieć, że strona spisu treści się nie pojawi.- Spis treści jest renderowany raz, w pozycji, w której go wstawiasz.
addTOC(pageIndex: 0)wstawia spis treści jako pierwszą stronę. Numery stron w wyrenderowanych wpisach używają zapisanej strony każdego wpisu, więc wstaw spis treści po wykonaniu wszystkich wywołańbookmark(). - Pominięte poziomy zniekształcają strukturę. Zwiększaj
$levelnajwyżej o jeden między kolejnymi zakładkami. Przeskok z poziomu 0 do poziomu 2 bez pośredniego poziomu 1 tworzy hierarchię, którą niektóre czytniki renderują nieprawidłowo.
Wydajność
Dział zatytułowany „Wydajność”Każde wywołanie bookmark() dołącza jeden element konspektu i jeden wpis spisu treści w czasie O(1), a każdy odczyt pozycji (getPage(), getY(), getNumPages()) jest dostępem do pola w stałym czasie w kontekście renderowania, bez iteracji. Drzewo konspektu i strona spisu treści są materializowane jednokrotnie: odpowiednio przy addTOC() i przy save(). Raport z setkami nagłówków pozostaje znacznie poniżej budżetu 2000 ms / 64 MB. Generowanie odbywa się w ramach procesu, bez przeglądarki bezgłowej i bez wywołań sieciowych.
Uwagi dotyczące bezpieczeństwa
Dział zatytułowany „Uwagi dotyczące bezpieczeństwa”Tytuły zakładek i strona spisu treści renderują wartości przekazane do bookmark(). Gdy te tytuły zawierają dane dostępne w czasie wykonywania, takie jak nazwa rozdziału z wiersza bazy danych lub pole API, ogranicz długość i oczyść ciąg, zanim trafi do bookmark(), dokładnie tak jak każdą wartość wyświetlaną w czytniku. Nie buduj tytułów z niezweryfikowanych danych żądania.
Silnik weryfikuje ścieżkę wyjściową przekazaną do save(): odrzuca opakowania strumieni (scheme://) oraz osadzone bajty null, a także rozwiązuje katalog nadrzędny, aby zablokować przechodzenie ścieżek, zgłaszając InvalidConfigException w przypadku któregokolwiek z tych warunków. Zachowaj tę weryfikację, przekazując kontrolowaną ścieżkę; nigdy nie przekazuj save() surowej nazwy pliku dostarczonej przez klienta. Gdy raportujesz InvalidConfigException wywołującemu, zapisz szczegóły po stronie serwera i zwróć ogólny komunikat zamiast rozwiązanej ścieżki.
Zgodność
Dział zatytułowany „Zgodność”Ten przepis nie wysuwa własnego twierdzenia o zgodności z ISO 32000-2. Semantyka konspektu i spisu treści, w tym konspekt dokumentu jako drzewo elementów konspektu oraz cele powiązane z tymi elementami, jest opisana w Dodawanie zakładek i spisu treści, gdzie znajdują się odpowiednie cytaty z klauzul. Dynamiczny wzorzec opisany tutaj zmienia tylko to, skąd pochodzi pozycja celu, a nie strukturę, która zostaje zapisana.
Profil odtwarzalności - strukturalny. Element /ID zwiastuna oraz atomy daty zmieniają się przy każdym zapisie; porównanie strukturalne usuwa te wartości. Ta strona dokumentuje, w jaki sposób NextPDF tworzy konspekt i spis treści na podstawie bieżącej pozycji kursora; nie wysuwa ogólnego twierdzenia o zgodności ze standardami.
Zobacz także
Dział zatytułowany „Zobacz także”- Dodawanie zakładek i spisu treści - statyczny odpowiednik tego przepisu
- Moduł nawigacji
- Domieszka HasPages - powierzchnia strony i pozycji
- Tworzenie dokumentu wielostronicowego
- Nagłówki i stopki