สร้างสารบัญจากโครงสร้างเอกสารขณะรันไทม์
ภาพรวมโดยสรุป
หัวข้อที่มีชื่อว่า “ภาพรวมโดยสรุป”เนื้อหาของคุณอาจถูกสร้างขึ้นขณะรันไทม์ เช่น บทต่างๆจากฐานข้อมูล ส่วนต่างๆจากการตอบกลับของ Application Programming Interface (API) หรือหัวเรื่องจากลูปที่ไม่อาจทราบล่วงหน้า คุณต้องการให้โครงร่างเอกสารและสารบัญที่คลิกได้ตรงกับเนื้อหานั้นพอดี โดยไม่ต้องดูแลรายการที่เขียนด้วยมืออีกชุดหนึ่งซึ่งอาจคลาดเคลื่อนได้
สูตรนี้สร้างโครงร่างแบบไดนามิก ขณะเขียนหัวเรื่องแต่ละรายการ คุณจะอ่านตำแหน่งเคอร์เซอร์และหน้าปัจจุบันแบบสดจากเอนจินด้วย getPage() getY() และ getNumPages() แล้วส่งค่าเหล่านั้นไปยัง bookmark() บุ๊กมาร์กจะผูกกับตำแหน่งที่คุณอ่านในขณะนั้น โครงร่างจึงติดตามเนื้อหาได้แม้การแบ่งหน้าจะเกิดในตำแหน่งที่ไม่คาดคิด ท้ายที่สุด addTOC() จะเรนเดอร์หน้าสารบัญจริงจากรายการชุดเดียวกัน
สิ่งที่ต้องมีก่อน: การติดตั้ง Core (composer require nextpdf/core:^3) และเนื้อหาที่คุณทราบโครงสร้างหัวเรื่องขณะเขียน ไม่ใช่ก่อนหน้านั้น
หน้านี้กล่าวถึงรูปแบบไดนามิกที่ขับเคลื่อนด้วยตำแหน่ง สำหรับกรณีแบบสแตติกที่คุณทราบหัวเรื่องและระดับทั้งหมดล่วงหน้า ให้อ่าน เพิ่มบุ๊กมาร์กและสารบัญ ก่อน สูตรนี้ใช้พื้นผิว bookmark() และ addTOC() ชุดเดียวกัน และจะไม่กล่าวซ้ำเรื่องพื้นฐานเหล่านั้น
การติดตั้ง
หัวข้อที่มีชื่อว่า “การติดตั้ง”composer require nextpdf/core:^3คุณไม่จำเป็นต้องติดตั้งส่วนขยายเสริมใดๆ พื้นผิวการนำทาง (bookmark() addTOC()) และตัวเข้าถึงตำแหน่ง (getPage() getY() getNumPages()) มีเสถียรภาพมาตั้งแต่ 1.2.0 และทำงานได้กับเมทริกซ์แบ็กพอร์ต 8.1 ถึง 8.4 ทั้งหมด
ภาพรวมเชิงแนวคิด
หัวข้อที่มีชื่อว่า “ภาพรวมเชิงแนวคิด”สารบัญแบบไดนามิกมีสองส่วนที่ต้องสอดคล้องกัน:
- ส่วนที่เป็นโครงร่าง (เรียกอีกอย่างว่าบุ๊กมาร์ก): ต้นไม้ที่ผู้อ่านเห็นในแถบนำทางด้านข้าง โดยแต่ละรายการจะกระโดดไปยังตำแหน่งหนึ่งในเอกสาร
- ส่วนที่เป็นสารบัญที่เรนเดอร์ออกมา: หน้าที่สร้างขึ้นเพื่อแสดงรายการชุดเดียวกันพร้อมหมายเลขหน้า
NextPDF ทำให้ทั้งสองส่วนตรงกันผ่านการเรียกเพียงครั้งเดียว bookmark($title, $level, $y) จะเพิ่มรายการโครงร่างหนึ่งรายการ และ รายการสารบัญหนึ่งรายการ โดยทั้งคู่ผูกกับหน้าปัจจุบันและตำแหน่งแนวตั้งปัจจุบัน คุณจึงไม่ต้องดูแลสองรายการแยกกัน
ส่วนที่เป็นไดนามิกคือ ตำแหน่งมาจากที่ใด สูตรแบบสแตติกจะส่งหัวเรื่องตามตัวอักษรตามลำดับในซอร์ส แต่ในที่นี้ คุณเขียนหัวเรื่อง แล้วถามเอนจินทันทีว่าเคอร์เซอร์อยู่ที่ใด:
getPage()จะคืนค่าดัชนีฐานศูนย์ของหน้าที่กำลังใช้งาน ก่อนที่จะเพิ่มหน้าแรก ค่าที่คืนคือ-1getNumPages()จะคืนค่าจำนวนหน้าทั้งหมด รวมถึงหน้าที่กำลังใช้งานซึ่งยังไม่ได้ฟลัชgetY()จะคืนค่าตำแหน่งเคอร์เซอร์แนวตั้งปัจจุบันในหน่วยผู้ใช้ โดยวัดเป็นระยะจากด้านบนของหน้าgetX()getPageHeight()และgetMargins()ช่วยให้เห็นภาพครบขึ้นเมื่อคุณต้องตัดสินใจว่าหัวเรื่องและเนื้อความบรรทัดแรกพอดีกันหรือไม่
อ่านค่าเหล่านั้น แล้วเรียก bookmark() การแบ่งหน้าอัตโนมัติอาจย้ายเคอร์เซอร์ไปยังหน้าใหม่ระหว่างหัวเรื่องสองรายการ การอ่านตำแหน่งกลับมาจึงทำให้ปลายทางของโครงร่างอยู่บนหน้าที่ถูกต้อง
รูปแบบทั้งหมดขึ้นอยู่กับลำดับเพียงข้อเดียว: เรียก bookmark() ณ จุดที่คุณต้องการให้เป็นปลายทางพอดี นั่นคือทันทีก่อนที่คุณจะเรนเดอร์ข้อความหัวเรื่อง หากคุณเขียนหัวเรื่องก่อนแล้วจึงบุ๊กมาร์กภายหลัง ค่า getY() ที่บันทึกไว้จะอยู่ใต้หัวเรื่องโดยตรง
พื้นผิว API
หัวข้อที่มีชื่อว่า “พื้นผิว API”สูตรนี้พึ่งพาเมท็อดเหล่านี้ของ \NextPDF\Core\Document:
bookmark(string $title, int $level = 0, float $y = -1): static- เพิ่มรายการโครงร่างและรายการสารบัญที่ระดับ$levelโดยผูกกับหน้าปัจจุบัน เมื่อใช้$y = -1ปลายทางจะใช้ค่า Y ของเคอร์เซอร์ปัจจุบัน ส่งค่า Y ที่ไม่เป็นลบเพื่อยึดปลายทางอย่างแม่นยำaddTOC(int $pageIndex = 0, string $title = ''): static- เรนเดอร์หน้าสารบัญจากรายการที่สะสมไว้ แล้วแทรกเข้าไปที่$pageIndexคืนค่าโดยไม่แทรกหน้าเมื่อไม่มีบุ๊กมาร์กgetPage(): int- ดัชนีฐานศูนย์ของหน้าที่กำลังใช้งานอยู่ (-1ก่อนหน้าแรก)getNumPages(): int- จำนวนหน้าทั้งหมด รวมถึงหน้าที่กำลังใช้งานอยู่ซึ่งยังไม่ได้ฟลัชgetY(): float- ค่า Y ของเคอร์เซอร์ปัจจุบันในหน่วยผู้ใช้ (ระยะห่างจากด้านบนของหน้า)getX(): float- ค่า X ของเคอร์เซอร์ปัจจุบันในหน่วยผู้ใช้getPageHeight(): float- ความสูงของหน้าปัจจุบันในหน่วยผู้ใช้getMargins(): \NextPDF\ValueObjects\Margin- ระยะขอบที่ใช้งานอยู่ (toprightbottomleft)setY(float $y): static- ย้ายเคอร์เซอร์ไปยังค่า Y ที่ระบุชัดเจนsetAutoPageBreak(bool $enabled, float $margin = 20): static- ควบคุมการแบ่งหน้าอัตโนมัติและเกณฑ์ระยะขอบล่าง
ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — เริ่มต้นอย่างรวดเร็ว”ตัวอย่างนี้เขียนสามส่วนจากรายการขณะรันไทม์ ในแต่ละรอบจะอ่านหน้าปัจจุบันแบบสดด้วย getPage() ก่อนสร้างบุ๊กมาร์ก ปลายทางของโครงร่างจึงยังคงถูกต้องหลังการแบ่งหน้าอัตโนมัติ
<?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');ผลลัพธ์ที่คาดหวังบนเทอร์มินัล โดยมีหนึ่งบรรทัดต่อส่วน:
Bookmarked 'Origins' on page index 0Bookmarked 'Method' on page index 0Bookmarked 'Results' on page index 0ตัวอย่างโค้ด — สำหรับใช้งานจริง
หัวข้อที่มีชื่อว่า “ตัวอย่างโค้ด — สำหรับใช้งานจริง”เวอร์ชันนี้สร้างโครงร่างสองระดับ (บทและส่วน) จากโครงสร้างขณะรันไทม์ที่ซ้อนกัน เวอร์ชันนี้รักษาให้หัวเรื่องอยู่กับบรรทัดแรกของเนื้อความด้วยการอ่านตำแหน่งก่อนเขียน และห่อหุ้มการสร้างไว้ในบล็อก try/catch สำหรับข้อยกเว้นของ NextPDF ที่เฉพาะเจาะจงที่สุด PageLayoutException ครอบคลุมความล้มเหลวฝั่งการสร้าง เช่น การเกินเพดานจำนวนหน้า save() จะยก InvalidConfigException สำหรับเส้นทางเอาต์พุตที่เขียนไม่ได้หรือไม่ปลอดภัย
<?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()จะคืนค่า-1ก่อนหน้าแรก เพิ่มหน้าแรกก่อนที่คุณจะอ่านตำแหน่งหรือเรียกbookmark()ตัวอย่างต่างๆเพิ่มหน้าไว้ตั้งแต่ต้น- ทำบุ๊กมาร์กก่อนหัวเรื่อง ไม่ใช่หลังจากนั้น
bookmark()ที่ใช้$y = -1จะบันทึกค่าgetY()ปัจจุบัน เรียกใช้ทันทีก่อนที่คุณจะเรนเดอร์หัวเรื่อง เพื่อให้ปลายทางตกอยู่ที่หัวเรื่อง ไม่ใช่บรรทัดที่อยู่ใต้มัน - การแบ่งหน้าอัตโนมัติย้ายปลายทางได้ เมื่อ
setAutoPageBreak()เปิดอยู่ การเรียกcell()หรือmultiCell()อาจฟลัชไปยังหน้าใหม่ อ่านgetPage()อีกครั้งในรอบถัดไปแทนการแคชค่าไว้ ปลายทางจะติดตามเนื้อหาเพราะbookmark()อ่านตำแหน่งแบบสดทุกครั้ง - สำรองพื้นที่ให้หัวเรื่องและบรรทัดแรกของมันอยู่ด้วยกัน หัวเรื่องที่อยู่พอดีท้ายหน้า ขณะที่เนื้อความของหัวเรื่องนั้นถูกตัดไปขึ้นหน้าถัดไป จะอ่านได้ไม่ดี ตัวอย่างสำหรับใช้งานจริงคำนวณความสูงที่เหลือจาก
getPageHeight()getMargins()->bottomและgetY()แล้วบังคับaddPage()ก่อนกำหนดเมื่อเหลือน้อยกว่าเกณฑ์ addTOC()บนเอกสารว่างจะไม่ทำสิ่งใด หากไม่เคยเรียกbookmark()เลยaddTOC()จะคืนค่าโดยไม่แทรกหน้า การป้องกันรายงานจากอินพุตว่างจึงไม่จำเป็น แม้ควรทราบไว้ว่าหน้าสารบัญจะไม่ปรากฏ- สารบัญถูกเรนเดอร์เพียงครั้งเดียว ณ ตำแหน่งที่คุณแทรกมัน
addTOC(pageIndex: 0)จะแทรกสารบัญเป็นหน้าแรก หมายเลขหน้าในรายการที่เรนเดอร์ออกมาจะใช้หน้าที่บันทึกไว้ของแต่ละรายการ ดังนั้นให้แทรกสารบัญหลังจากการเรียกbookmark()ทั้งหมดทำงานเสร็จแล้ว - การข้ามระดับดูเหมือนผิดรูปแบบ เพิ่มค่า
$levelได้สูงสุดทีละหนึ่งระหว่างบุ๊กมาร์กที่ต่อเนื่องกัน การกระโดดจากระดับ 0 ไปยังระดับ 2 โดยไม่มีระดับ 1 คั่นกลางจะสร้างลำดับชั้นที่โปรแกรมอ่านบางตัวเรนเดอร์ผิด
ประสิทธิภาพ
หัวข้อที่มีชื่อว่า “ประสิทธิภาพ”การเรียก bookmark() แต่ละครั้งจะเพิ่มรายการโครงร่างหนึ่งรายการและรายการสารบัญหนึ่งรายการในเวลา O(1) และการอ่านตำแหน่งแต่ละครั้ง (getPage() getY() getNumPages()) เป็นการเข้าถึงฟิลด์แบบเวลาคงที่บนคอนเท็กซ์การเรนเดอร์ โดยไม่ต้องไล่ผ่านข้อมูล ต้นไม้โครงร่างและหน้าสารบัญแต่ละส่วนถูกสร้างเป็นรูปธรรมเพียงครั้งเดียว: ที่ addTOC() และที่ save() ตามลำดับ รายงานที่มีหัวเรื่องหลายร้อยรายการยังคงอยู่ภายในงบประมาณ 2000 ms / 64 MB ได้อย่างสบาย การสร้างทำงานภายในโปรเซส โดยไม่มีเบราว์เซอร์แบบไร้ส่วนติดต่อและไม่มีการเรียกผ่านเครือข่าย
หมายเหตุด้านความปลอดภัย
หัวข้อที่มีชื่อว่า “หมายเหตุด้านความปลอดภัย”ชื่อบุ๊กมาร์กและหน้าสารบัญจะเรนเดอร์ค่าที่คุณส่งให้ bookmark() เมื่อชื่อเหล่านั้นมีข้อมูลขณะรันไทม์ เช่น ชื่อบทจากแถวฐานข้อมูลหรือฟิลด์ของ API ให้จำกัดความยาวและทำความสะอาดสตริงก่อนส่งถึง bookmark() เช่นเดียวกับค่าใดๆที่แสดงในโปรแกรมอ่าน อย่าสร้างชื่อจากอินพุตของคำขอที่ยังไม่ได้ตรวจสอบ
เอนจินตรวจสอบเส้นทางเอาต์พุตที่ส่งให้ save() โดยปฏิเสธ stream wrapper (scheme://) และ null byte ที่ฝังอยู่ และแปลงไดเรกทอรีแม่เพื่อสกัดกั้น path traversal โดยจะยก InvalidConfigException ในเงื่อนไขใดเงื่อนไขหนึ่งเหล่านี้ ให้คงการตรวจสอบนั้นไว้ด้วยการส่งเฉพาะเส้นทางที่คุณควบคุม อย่าส่งชื่อไฟล์ดิบที่มาจากไคลเอนต์ให้ save() เป็นอันขาด เมื่อคุณรายงาน InvalidConfigException ไปยังผู้เรียก ให้บันทึกรายละเอียดไว้ฝั่งเซิร์ฟเวอร์และคืนข้อความทั่วไปแทนเส้นทางที่แปลงแล้ว
ความสอดคล้อง
หัวข้อที่มีชื่อว่า “ความสอดคล้อง”สูตรนี้ไม่ได้ยืนยันการอ้างความสอดคล้อง ISO 32000-2 ใดๆด้วยตนเอง ความหมายของโครงร่างและสารบัญ รวมถึงโครงร่างเอกสารในฐานะต้นไม้ของรายการโครงร่างและปลายทางที่เชื่อมโยงกับรายการเหล่านั้น อธิบายไว้ใน เพิ่มบุ๊กมาร์กและสารบัญ ซึ่งมีการอ้างอิงข้อกำหนดที่เกี่ยวข้อง รูปแบบไดนามิกในที่นี้เปลี่ยนแปลงเฉพาะ ตำแหน่งปลายทางมาจากที่ใด ไม่ใช่โครงสร้างที่ถูกเขียนลงไป
โปรไฟล์การทำซ้ำได้ - เชิงโครงสร้าง ค่า /ID ในเทรลเลอร์และอะตอมของวันที่จะแปรผันทุกครั้งที่บันทึก การเปรียบเทียบเชิงโครงสร้างจะตัดค่าเหล่านั้นออก หน้านี้บันทึกไว้ว่า NextPDF สร้างโครงร่างและสารบัญจากเคอร์เซอร์แบบสดอย่างไร แต่ไม่ได้ยืนยันการอ้างความสอดคล้องตามมาตรฐานแบบครอบคลุม
ดูเพิ่มเติม
หัวข้อที่มีชื่อว่า “ดูเพิ่มเติม”- เพิ่มบุ๊กมาร์กและสารบัญ - คู่มือแบบสแตติกของสูตรนี้
- โมดูล Navigation
- คอนเซิร์น HasPages - พื้นผิวหน้าและตำแหน่ง
- สร้างเอกสารหลายหน้า
- ส่วนหัวและส่วนท้าย