Tạo mục lục từ cấu trúc tài liệu lúc chạy
Tổng quan nhanh
Phần tiêu đề “Tổng quan nhanh”Nội dung của bạn có thể hình thành lúc chạy: các chương từ một cơ sở dữ liệu, các phần từ phản hồi của một giao diện lập trình ứng dụng (API), hoặc các tiêu đề từ một vòng lặp mà bạn không thể biết trước. Bạn cần dàn ý tài liệu và mục lục có thể bấm khớp chính xác với nội dung đó, mà không phải duy trì thêm một danh sách viết tay có thể bị lệch khỏi trạng thái đồng bộ.
Công thức này dựng dàn ý một cách động. Khi viết từng tiêu đề, bạn đọc con trỏ trực tiếp và trang từ engine bằng getPage(), getY() và getNumPages(), rồi truyền những giá trị đó cho bookmark(). Bookmark gắn với vị trí được đọc tại thời điểm đó, nên dàn ý đi theo nội dung ngay cả khi các ngắt trang rơi vào những chỗ bất ngờ. Cuối cùng, addTOC() kết xuất một trang mục lục thực sự từ chính các mục đó.
Điều kiện tiên quyết: một bản cài đặt Core (composer require nextpdf/core:^3) và nội dung mà bạn khám phá cấu trúc tiêu đề trong khi viết, chứ không phải trước đó.
Trang này trình bày mẫu động, dựa trên vị trí. Với trường hợp tĩnh, khi bạn biết trước mọi tiêu đề và cấp của nó, hãy đọc Thêm bookmark và mục lục trước. Công thức này dùng cùng bề mặt bookmark() và addTOC() nên không lặp lại những phần cơ bản đó.
Cài đặt
Phần tiêu đề “Cài đặt”composer require nextpdf/core:^3Bạn không cần bất kỳ phần mở rộng tùy chọn nào. Bề mặt điều hướng (bookmark(), addTOC()) và các bộ truy cập vị trí (getPage(), getY(), getNumPages()) đã ổn định kể từ 1.2.0 và chạy trên ma trận backport từ 8.1 đến 8.4.
Tổng quan khái niệm
Phần tiêu đề “Tổng quan khái niệm”Một mục lục động có hai phần cần khớp với nhau:
- Phần Dàn ý (còn gọi là bookmark): cây mà người đọc thấy trong thanh bên điều hướng, nơi mỗi mục nhảy đến một vị trí trong tài liệu.
- Phần Mục lục được kết xuất: một trang được tạo ra liệt kê chính các mục đó cùng số trang của chúng.
NextPDF giữ cả hai luôn đồng bộ qua một lệnh gọi duy nhất. bookmark($title, $level, $y) thêm một mục dàn ý và một mục lục, cả hai đều gắn với trang hiện tại và vị trí dọc hiện tại. Bạn không phải duy trì hai danh sách.
Điểm động nằm ở vị trí đến từ đâu. Một công thức tĩnh truyền các tiêu đề nguyên văn theo thứ tự nguồn. Ở đây, bạn viết một tiêu đề, rồi ngay lập tức hỏi engine xem con trỏ đã dừng ở đâu:
getPage()trả về chỉ số bắt đầu từ 0 của trang đang hoạt động. Trước khi trang đầu tiên được thêm, nó trả về-1.getNumPages()trả về tổng số trang, bao gồm cả trang đang hoạt động chưa được flush.getY()trả về con trỏ dọc hiện tại theo đơn vị người dùng, được đo là khoảng cách từ đỉnh trang.getX(),getPageHeight()vàgetMargins()bổ sung cho bức tranh khi bạn cần quyết định liệu một tiêu đề và dòng văn bản nội dung đầu tiên của nó có vừa với nhau hay không.
Đọc những giá trị đó, rồi gọi bookmark(). Ngắt trang tự động có thể chuyển con trỏ sang một trang mới giữa hai tiêu đề, nên việc đọc lại vị trí giúp đích đến của dàn ý nằm đúng trang.
Một điểm về thứ tự chi phối toàn bộ mẫu này: gọi bookmark() tại đúng vị trí bạn muốn dùng làm đích đến, tức là ngay trước khi bạn kết xuất văn bản tiêu đề. Nếu bạn viết tiêu đề trước rồi mới bookmark sau, giá trị getY() được ghi lại sẽ nằm ngay bên dưới tiêu đề.
Bề mặt API
Phần tiêu đề “Bề mặt API”Công thức này dựa vào các phương thức \NextPDF\Core\Document sau:
bookmark(string $title, int $level = 0, float $y = -1): static- thêm một mục dàn ý và một mục lục ở$level, gắn với trang hiện tại. Với$y = -1, đích đến là Y của con trỏ hiện tại; truyền một Y không âm để ghim đích đến chính xác.addTOC(int $pageIndex = 0, string $title = ''): static- kết xuất một trang mục lục từ các mục đã tích lũy và chèn nó vào tại$pageIndex. Trả về mà không chèn trang khi không có bookmark nào tồn tại.getPage(): int- chỉ số bắt đầu từ 0 của trang đang hoạt động (-1trước trang đầu tiên).getNumPages(): int- tổng số trang, bao gồm cả trang đang hoạt động chưa được flush.getY(): float- Y của con trỏ hiện tại theo đơn vị người dùng (khoảng cách từ đỉnh trang).getX(): float- X của con trỏ hiện tại theo đơn vị người dùng.getPageHeight(): float- chiều cao của trang hiện tại theo đơn vị người dùng.getMargins(): \NextPDF\ValueObjects\Margin- các lề đang hoạt động (top,right,bottom,left).setY(float $y): static- di chuyển con trỏ đến một Y rõ ràng.setAutoPageBreak(bool $enabled, float $margin = 20): static- kiểm soát ngắt trang tự động và ngưỡng lề dưới của nó.
Mẫu mã - bắt đầu nhanh
Phần tiêu đề “Mẫu mã - bắt đầu nhanh”Mẫu này viết ba phần từ một danh sách lúc chạy. Ở mỗi vòng lặp, mẫu đọc trang hiện tại bằng getPage() trước khi bookmark, để đích đến của dàn ý vẫn đúng sau một ngắt trang tự động.
<?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');Kết quả dự kiến trên terminal, mỗi phần một dòng:
Bookmarked 'Origins' on page index 0Bookmarked 'Method' on page index 0Bookmarked 'Results' on page index 0Mẫu mã - sản xuất
Phần tiêu đề “Mẫu mã - sản xuất”Phiên bản này dựng một dàn ý hai cấp (các chương và các phần) từ một cấu trúc lúc chạy lồng nhau. Nó giữ tiêu đề đi cùng dòng nội dung đầu tiên bằng cách đọc vị trí trước khi ghi ra tài liệu, đồng thời bọc quá trình tạo trong các khối try/catch cho những ngoại lệ NextPDF cụ thể nhất. PageLayoutException bao quát lỗi phát sinh ở phía tạo, chẳng hạn như vượt quá giới hạn trần trang. save() ném ra InvalidConfigException cho một đường dẫn đầu ra không thể ghi hoặc không an toàn.
<?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);}Trường hợp biên & điểm cần lưu ý
Phần tiêu đề “Trường hợp biên & điểm cần lưu ý”getPage()trả về-1trước trang đầu tiên. Thêm trang đầu tiên trước khi bạn đọc vị trí hoặc gọibookmark(). Các mẫu thêm một trang ngay từ đầu.- Bookmark trước tiêu đề, không phải sau.
bookmark()với$y = -1ghi lạigetY()hiện tại. Gọi nó ngay trước khi bạn kết xuất tiêu đề để đích đến rơi vào tiêu đề, chứ không phải dòng ngay bên dưới nó. - Ngắt trang tự động làm dịch chuyển đích đến. Khi
setAutoPageBreak()đang bật, một lệnh gọicell()hoặcmultiCell()có thể flush sang một trang mới. Đọc lạigetPage()ở vòng lặp tiếp theo thay vì lưu vào bộ nhớ tạm. Đích đến đi theo nội dung vìbookmark()đọc vị trí trực tiếp mỗi lần. - Dành chỗ để tiêu đề và dòng đầu tiên của nó đi cùng nhau. Một tiêu đề nằm sát chân trang trong khi nội dung của nó cuộn sang trang tiếp theo thì sẽ khó đọc. Mẫu sản xuất tính chiều cao còn lại từ
getPageHeight(),getMargins()->bottomvàgetY(), rồi buộc mộtaddPage()sớm khi còn lại ít hơn một ngưỡng. addTOC()trên một tài liệu rỗng thì không làm gì cả. Nếu không có lệnh gọibookmark()nào chạy,addTOC()trả về mà không chèn trang. Do đó không bắt buộc phải bảo vệ báo cáo khỏi đầu vào rỗng, mặc dù cũng nên biết rằng trang mục lục sẽ không xuất hiện.- Mục lục được kết xuất một lần, tại vị trí bạn chèn nó.
addTOC(pageIndex: 0)chèn mục lục làm trang đầu tiên. Số trang trong các mục được kết xuất dùng trang đã ghi lại của từng mục, nên hãy chèn mục lục sau khi mọi lệnh gọibookmark()đã chạy. - Bỏ qua cấp trông giống lỗi định dạng. Tăng
$levelnhiều nhất một đơn vị giữa các bookmark liên tiếp. Nhảy từ cấp 0 lên cấp 2 mà không có cấp 1 ở giữa tạo ra một phân cấp mà một số trình đọc kết xuất không chính xác.
Hiệu năng
Phần tiêu đề “Hiệu năng”Mỗi lệnh gọi bookmark() thêm một mục dàn ý và một mục lục trong thời gian O(1), và mỗi lần đọc vị trí (getPage(), getY(), getNumPages()) là một truy cập trường thời gian hằng số trên ngữ cảnh kết xuất, không duyệt cây. Cây dàn ý và trang mục lục mỗi cái được hiện thực hóa một lần: lần lượt tại addTOC() và tại save(). Một báo cáo với hàng trăm tiêu đề vẫn dễ dàng nằm trong ngân sách 2000 ms / 64 MB. Việc tạo chạy trong tiến trình, không có trình duyệt headless và không có lệnh gọi mạng.
Lưu ý về bảo mật
Phần tiêu đề “Lưu ý về bảo mật”Tiêu đề bookmark và trang mục lục kết xuất các giá trị bạn truyền cho bookmark(). Khi những tiêu đề đó mang dữ liệu lúc chạy, chẳng hạn như tên chương từ một hàng cơ sở dữ liệu hoặc một trường API, hãy giới hạn độ dài và làm sạch chuỗi trước khi nó đến bookmark(), đúng như bạn làm với bất kỳ giá trị nào hiển thị trong trình đọc. Không dựng tiêu đề từ đầu vào yêu cầu chưa được xác thực.
Engine xác thực đường dẫn đầu ra được truyền cho save(): nó từ chối các stream wrapper (scheme://) và các byte null nhúng, đồng thời phân giải thư mục cha để chặn duyệt đường dẫn, ném ra InvalidConfigException khi gặp bất kỳ điều kiện nào trong số này. Hãy giữ cho việc xác thực đó hoạt động bằng cách truyền một đường dẫn bạn kiểm soát; đừng bao giờ đưa cho save() một tên tệp thô do client cung cấp. Khi bạn báo cáo một InvalidConfigException cho bên gọi, hãy ghi nhật ký chi tiết ở phía máy chủ và trả về một thông báo chung chung thay vì đường dẫn đã phân giải.
Tuân thủ
Phần tiêu đề “Tuân thủ”Công thức này không tự đưa ra bất kỳ tuyên bố tuân thủ ISO 32000-2 nào. Ngữ nghĩa của dàn ý và mục lục, bao gồm dàn ý tài liệu như một cây các mục dàn ý và các đích đến liên kết với những mục đó, được mô tả trong Thêm bookmark và mục lục, vốn mang theo các trích dẫn điều khoản liên quan. Mẫu động ở đây chỉ thay đổi vị trí đích đến đến từ đâu, chứ không phải cấu trúc được ghi.
Hồ sơ khả năng tái lập - cấu trúc. Atom /ID ở trailer và các atom ngày tháng thay đổi theo mỗi lần lưu; một phép so sánh cấu trúc sẽ loại bỏ những giá trị đó. Trang này ghi lại cách NextPDF tạo ra dàn ý và mục lục từ con trỏ trực tiếp; nó không đưa ra một tuyên bố tuân thủ tiêu chuẩn toàn diện.
Xem thêm
Phần tiêu đề “Xem thêm”- Thêm bookmark và mục lục - phiên bản tĩnh tương ứng với công thức này
- Mô-đun Navigation
- Concern HasPages - bề mặt trang và vị trí
- Dựng một tài liệu nhiều trang
- Đầu trang và chân trang