ข้ามไปยังเนื้อหา

การวินิจฉัยตัวแยกวิเคราะห์ PDF ขั้นสูง

เส้นทางการนำเข้าของ Artisan จะอ่านไฟล์ Portable Document Format (PDF) ที่สร้างโดย Chrome แล้วนำหน้าหนึ่งหน้าเข้าสู่เอกสาร NextPDF เมื่ออินพุตที่ซับซ้อนทำให้การนำเข้าล้มเหลว ให้ตรวจสอบส่วนที่อยู่ใต้ PageImporter::import() ลงไปจนถึงคลาสตัวแยกวิเคราะห์ที่อ่านไฟล์ทีละไบต์

คู่มือนี้ครอบคลุมพื้นผิวตัวแยกวิเคราะห์ระดับล่างในเนมสเปซ NextPDF\Parser ได้แก่ PdfReader, PdfTokenizer, CrossRefParser, StreamDecoder, ResourceCollector, RevisionExtractor รวมถึงอ็อบเจกต์ค่า PdfObject และ RevisionXRefTable สัญลักษณ์ทุกตัวที่แสดงในที่นี้มีอยู่ใน nextpdf/artisan คู่มือนี้อธิบายตัวแยกวิเคราะห์ตามที่ถูกสร้างขึ้นจริง ไม่ใช่อินเทอร์เฟซในอุดมคติ

ใช้คู่มือนี้ได้ทั้งเป็นคำอธิบายและคู่มือปฏิบัติ คู่มือนี้แสดงให้เห็นว่าส่วนประกอบแต่ละส่วนทำงานร่วมกันอย่างไร จากนั้นจึงพาคุณตรวจสอบรีวิชันแบบอัปเดตทีละส่วน สำหรับขอบเขตการนำเข้าที่อยู่เหนือเลเยอร์นี้ โปรดดู คู่มือนักพัฒนาของ Artisan

ใช้พื้นผิวตัวแยกวิเคราะห์เฉพาะเมื่อเส้นทางการนำเข้าปกติล้มเหลวแล้ว และคุณต้องการค้นหาสาเหตุ กรณีที่พบได้บ่อยได้แก่:

  • PageImporter::import() โยน NextPDF\Artisan\Exception\PdfParseException และคุณจำเป็นต้องทราบว่าตารางอ้างอิงไขว้ ตัวกรองสตรีม หรือ page tree เป็นต้นเหตุ
  • การอัปเกรด Chrome เปลี่ยนรูปแบบเอาต์พุต เช่น เมื่อตารางอ้างอิงไขว้แบบดั้งเดิมกลายเป็นสตรีมอ้างอิงไขว้ หรือในทางกลับกัน และฟิกซ์เจอร์ของคุณไม่ตรงกันอีก
  • คุณได้รับ PDF จากภายนอกที่ Chrome ไม่ได้สร้างขึ้น และต้องการยืนยันว่าตัวแยกวิเคราะห์อ่านได้หรือไม่
  • คุณกำลังวิเคราะห์เอกสารที่ถูกอัปเดตทีละส่วน และต้องการช่วงไบต์ต่อรีวิชันหรือการมองเห็นอ็อบเจกต์

หากคุณกำลังเขียนการผสานรวมกับเรนเดอเรอร์ตามปกติ คุณไม่จำเป็นต้องใช้พื้นผิวนี้ ตัวแยกวิเคราะห์เป็นเครื่องมือวินิจฉัยภายใน ไม่ใช่ไลบรารี PDF เอนกประสงค์ ตัวแยกวิเคราะห์ไม่รองรับ PDF ที่เข้ารหัสลับ ตาราง hint แบบ linearized หรือการอัปเดตทีละส่วนที่นิยามอ็อบเจกต์ซ้ำจนขัดแย้งกัน

ตัวแยกวิเคราะห์เป็นชุดคลาสขนาดเล็กที่แต่ละคลาสมีความรับผิดชอบเดียว PdfReader คือจุดเริ่มต้น ส่วนคลาสอื่นๆเป็นคลาสร่วมงานที่ตัวแยกวิเคราะห์นี้สร้างหรือเรียกใช้

คลาสความรับผิดชอบเมธอดหลัก
PdfReaderอ่านโครงสร้างไฟล์ แก้ไขอ็อบเจกต์ และท่องไปใน page treeparse(), getObject(), getTrailer(), getObjectNumbers(), getPage(), getPageContentStream(), getPageResources(), getPageMediaBox(), resolveRef(), collectPageResources(), getRevisionCount(), getRevisionXRef(), getRevisions()
PdfTokenizerวิเคราะห์ไวยากรณ์เชิงคำศัพท์ตาม ISO 32000-2:2020 §7.2 ได้แก่ names, strings, numbers, dictionaries, arrays และ referencesreadToken(), readValue(), readName(), readNumber(), readDictionary(), readArray(), readStreamData(), peek(), skipWhitespace(), getOffset(), setOffset()
CrossRefParserแยกวิเคราะห์ตารางอ้างอิงไขว้แบบดั้งเดิมและสตรีมอ้างอิงไขว้parseXRefTable(), parseXRefStream()
StreamDecoderถอดรหัสไบต์ของสตรีมตาม /Filterdecode() (static)
ResourceCollectorท่องไปใน Resources tree แบบเรียกซ้ำและรวบรวมอ็อบเจกต์ทางอ้อมทุกตัวที่เข้าถึงได้traverse(), getCollected()
RevisionExtractorแบ่งไฟล์ที่ถูกอัปเดตทีละส่วนออกเป็นช่วงไบต์ต่อรีวิชันextractRevision() (static), getRevisionBoundaries() (static)
PdfObjectอ็อบเจกต์ทางอ้อมที่แยกวิเคราะห์แล้วแบบเปลี่ยนแปลงไม่ได้ (dictionary พร้อมสตรีมเสริม)get(), getRef(), getArray(), getType(), getSubtype(), hasStream(), getDictionary(), getRawStreamData(), getRawDictionaryBytes()
RevisionXRefTableสแนปช็อตการอ้างอิงไขว้ต่อรีวิชันแบบเปลี่ยนแปลงไม่ได้getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize()

สร้าง \NextPDF\Parser\PdfReader ด้วยไบต์ PDF ดิบ แล้วเรียก parse() ก่อนเรียกเมธอดอื่นใด parse() ตรวจสอบส่วนหัว %PDF- ค้นหา startxref ในส่วนท้ายของไฟล์ และไล่ตามห่วงโซ่การอ้างอิงไขว้โดยติดตามลิงก์ /Prev

หลังจาก parse() แล้ว ตัวอ่านจะเปิดเผยเมธอดเป็นสามกลุ่ม:

  • การเข้าถึงอ็อบเจกต์ getObject(int $objNum) คืนค่า PdfObject โดยแก้ไขรายการ Type 2 (อ็อบเจกต์ที่เก็บอยู่ภายใน object stream) ให้โดยอัตโนมัติ getObjectNumbers() คืนค่า list<int> ที่เรียงลำดับของหมายเลขอ็อบเจกต์ที่ไม่ใช่อิสระทุกตัว resolveRef(mixed $value) ติดตามการอ้างอิงทางอ้อมหนึ่งรายการ ค่าโดยตรงจะผ่านไปโดยไม่เปลี่ยนแปลง
  • การเข้าถึงหน้า getPage(int $pageIndex) แก้ไข catalog ท่องไปใน /Pages และคืนค่าหน้าตามดัชนีฐานศูนย์ getPageContentStream(), getPageResources() และ getPageMediaBox() ดึงส่วนที่ PageImporter ต้องการ collectPageResources() คืนค่า array<int, PdfObject> สำหรับอ็อบเจกต์ทุกตัวที่เข้าถึงได้จาก Resources และ Contents ของหน้า
  • การเข้าถึงรีวิชัน getRevisionCount() คืนค่าจำนวนรีวิชันแบบอัปเดตทีละส่วน ไฟล์ที่มีรีวิชันเดียวจะคืนค่า 1 getRevisionXRef(int $index) คืนค่า RevisionXRefTable หนึ่งรายการ (ดัชนี 0 คือรายการที่ใหม่ที่สุด) getRevisions() คืนค่า list<RevisionXRefTable> ฉบับเต็ม

PdfTokenizer อ่านสตรีมไบต์ โดยทั่วไปคุณแทบไม่ต้องสร้างอินสแตนซ์เอง เพราะ PdfReader และ CrossRefParser มีอินสแตนซ์ของตนอยู่แล้ว ให้ตรวจสอบเลเยอร์นี้เมื่อการแยกวิเคราะห์ล้มเหลวเพราะโทเคนผิดรูปแบบ มีพฤติกรรมสองอย่างที่สำคัญต่อการวินิจฉัย:

  • ขีดจำกัดด้านความปลอดภัยคือค่าคงที่ ไม่ใช่การกำหนดค่า ตัวแบ่งโทเคนจำกัดการซ้อนกันของ literal string การซ้อนกันของ dictionary และ array ความยาวคีย์เวิร์ด และจำนวนสมาชิกใน array เมื่ออินพุตเกินขีดจำกัด ตัวแบ่งโทเคนจะโยน PdfParseException และระบุชื่อขีดจำกัดไว้ในข้อความ อินพุตที่ถูกประดิษฐ์ขึ้นซึ่งไปสะดุดขีดจำกัดเหล่านี้คือการป้องกันที่ทำงานตามที่ออกแบบไว้ ไม่ใช่ข้อบกพร่องของตัวแยกวิเคราะห์
  • readValue() จัดเส้นทางการแยกวิเคราะห์ เมธอดนี้ตรวจสอบไบต์ถัดไปและมอบหมายงานให้ readName(), readLiteralString(), readHexString(), readArray(), readDictionary() หรือตัวอ่าน number/reference การอ้างอิงทางอ้อม N G R จะถูกคืนค่าเป็นรูปทรง array ['type' => 'ref', 'num' => N, 'gen' => G] PdfObject::getRef() และ PdfReader::resolveRef() รู้จักรูปทรงนี้

CrossRefParser แยกวิเคราะห์รูปแบบทั้งสองที่ Chrome สามารถสร้างได้:

  • parseXRefTable() อ่านตาราง xref แบบดั้งเดิม (รูปแบบ PDF 1.x) ได้แก่ ส่วนหัวของ subsection รายการขนาดคงที่ 20 ไบต์ และ dictionary trailer ที่ตามมา
  • parseXRefStream() อ่านสตรีมอ้างอิงไขว้ (PDF 2.0, ISO 32000-2:2020 §7.5.8) ได้แก่ อ็อบเจกต์ทางอ้อมที่มี /Type /XRef array ความกว้างฟิลด์ /W และสตรีมไบนารีของรายการ

ทั้งสองคืนค่ารูปทรงเดียวกัน: array{xref: array<int, ...>, trailer: array<string, mixed>, prevOffset: int|null} PdfReader::parse() ตัดสินใจว่าจะเรียกตัวแยกวิเคราะห์ตัวใดโดยตรวจดูสี่ไบต์ที่ออฟเซ็ตของการอ้างอิงไขว้: xref เลือกตัวแยกวิเคราะห์ตาราง ส่วนค่าอื่นๆจะถูกถือว่าเป็นอ็อบเจกต์สตรีม ตัวแยกวิเคราะห์ทั้งสองบังคับเพดานหนึ่งล้านรายการต่อ subsection เพื่อปฏิเสธจำนวนที่ถูกปลอมแปลงซึ่งมิฉะนั้นจะทำให้ตัวแยกวิเคราะห์ทำงานมากเกินไป

StreamDecoder::decode(string $data, string|array $filter) เป็น static และรับตัวกรองหนึ่งตัวหรือรายการตัวกรองที่เชื่อมต่อกัน รองรับเฉพาะตัวกรองที่ printToPDF ของ Chrome สร้างขึ้นเท่านั้น:

  • FlateDecode (zlib พร้อม raw-deflate fallback)
  • ASCIIHexDecode
  • ASCII85Decode

ชื่อตัวกรองอื่นใดจะโยน PdfParseException พร้อม Unsupported stream filter ตัวถอดรหัสจำกัดเอาต์พุตที่คลายการบีบอัดไว้ที่ 16 MiB เพื่อลดความเสี่ยงจาก decompression bomb เอาต์พุตที่มีขนาดเกินจะโยนข้อยกเว้นแทนการจัดสรรหน่วยความจำโดยไม่มีขีดจำกัด เมื่อ PdfReader อ่านสตรีมแล้วการถอดรหัสโยนข้อยกเว้น ตัวอ่านจะถอยกลับไปใช้ไบต์สตรีมดิบ ดังนั้นตัวกรองที่ไม่ถูกต้องหนึ่งตัวจะไม่ทำให้การแยกวิเคราะห์ทั้งหมดถูกยกเลิก

ResourceCollector ถูกสร้างด้วย PdfReader และถูกเรียกผ่าน PdfReader::collectPageResources() เมธอด traverse() ของคลาสนี้ท่องไปในค่าแบบเรียกซ้ำ ติดตามการอ้างอิง ['type' => 'ref'] ทุกตัวผ่าน getObject() และบันทึกอ็อบเจกต์ที่แก้ไขแล้วแต่ละตัวหนึ่งครั้งใน array<int, PdfObject> ที่ใช้หมายเลขอ็อบเจกต์เป็นคีย์ คลาสนี้จำกัดความลึกของการเรียกซ้ำและข้ามการอ้างอิงที่แก้ไขไม่ได้อย่างเงียบๆ ดังนั้นการอ้างอิงค้างอยู่หนึ่งรายการจึงให้ผลเป็นการรวบรวมบางส่วนแทนที่จะล้มเหลวอย่างรุนแรง

PDF ที่ถูกลงนาม ใส่คำอธิบายประกอบ หรือแก้ไขด้วยวิธีอื่นหลังการสร้างจะมีการอัปเดตทีละส่วน การแก้ไขแต่ละครั้งจะผนวกส่วนการอ้างอิงไขว้และ trailer ใหม่ แล้วจบด้วยเครื่องหมาย %%EOF RevisionExtractor ทำงานทั้งหมดผ่านเมธอด static บน PdfReader ที่แยกวิเคราะห์แล้ว:

  • extractRevision(string $pdfData, PdfReader $reader, int $revision) คืนค่าไฟล์ที่ตัดที่ขอบเขต %%EOF ของรีวิชันที่ร้องขอ รีวิชัน 0 (ใหม่ที่สุด) คืนค่าไฟล์ทั้งหมด ดัชนีที่สูงขึ้นคืนค่าสแนปช็อตที่เก่าลงเรื่อยๆ
  • getRevisionBoundaries(string $pdfData, PdfReader $reader) คืนค่า list<array{revision, startByte, endByte, sizeBytes}> ที่อธิบายช่วงไบต์ที่แต่ละรีวิชันมีส่วนร่วม

การแยกส่วนนี้เป็นไปโดยตั้งใจ การดึงรีวิชันที่เก่ากว่าจะเปิดเผยเฉพาะอ็อบเจกต์ที่มองเห็นได้จนถึงจุดนั้น ซึ่งช่วยสกัดกั้นการโจมตีแบบ hybrid cross-reference ที่รีวิชันถัดๆไปนิยามอ็อบเจกต์ก่อนหน้าซ้ำ

ขั้นตอนนี้ใช้ตรวจสอบประวัติรีวิชันของ PDF ที่อาจถูกแก้ไขหลังจาก Chrome สร้างขึ้น ตัวอย่างนี้จัดรูปสำหรับการใช้งานจริง: ประกาศ strict types ใช้ type hint ครบถ้วน ตรวจสอบความถูกต้องของอินพุต และจับข้อยกเว้นที่เฉพาะเจาะจงที่สุด

  1. อ่านไบต์ PDF เข้าสู่หน่วยความจำ และปฏิเสธอินพุตที่ว่างเปล่าก่อนสร้างตัวอ่าน
  2. สร้าง \NextPDF\Parser\PdfReader และเรียก parse()
  3. อ่าน getRevisionCount() ค่า 1 หมายถึงไฟล์ที่มีรีวิชันเดียวโดยไม่มีการอัปเดตทีละส่วน
  4. สำหรับแต่ละรีวิชัน ให้อ่าน RevisionXRefTable ของรีวิชันนั้น แล้วตรวจสอบ getActiveObjectCount(), hasRootUpdate() และ getSize()
  5. คำนวณช่วงไบต์ต่อรีวิชันด้วย RevisionExtractor::getRevisionBoundaries()
  6. จับ PdfParseException ซึ่งเป็นข้อยกเว้นที่เฉพาะเจาะจงที่สุดที่ตัวแยกวิเคราะห์โยน และแสดงข้อความวินิจฉัย
examples/inspect-revisions.php
<?php
declare(strict_types=1);
namespace App\Pdf\Diagnostics;
use NextPDF\Artisan\Exception\PdfParseException;
use NextPDF\Parser\PdfReader;
use NextPDF\Parser\RevisionExtractor;
use NextPDF\Parser\RevisionXRefTable;
/**
* Inspect the incremental-update history of a PDF file.
*
* @return list<array{revision: int, activeObjects: int, rootUpdate: bool, size: int, startByte: int, endByte: int, sizeBytes: int}>
*
* @throws PdfParseException If the file is not a readable PDF.
*/
function inspectRevisions(string $path): array
{
$pdfData = \file_get_contents($path);
if ($pdfData === false || $pdfData === '') {
throw new PdfParseException("Cannot read PDF bytes from path: {$path}");
}
$reader = new PdfReader($pdfData);
$reader->parse();
$boundaries = RevisionExtractor::getRevisionBoundaries($pdfData, $reader);
$report = [];
foreach ($reader->getRevisions() as $table) {
\assert($table instanceof RevisionXRefTable);
$index = $table->index;
$boundary = $boundaries[$index];
$report[] = [
'revision' => $index,
'activeObjects' => $table->getActiveObjectCount(),
'rootUpdate' => $table->hasRootUpdate(),
'size' => $table->getSize(),
'startByte' => $boundary['startByte'],
'endByte' => $boundary['endByte'],
'sizeBytes' => $boundary['sizeBytes'],
];
}
return $report;
}

ตัวอ่านจัดเรียงรีวิชันจากใหม่ที่สุด (index0) ไปยังเก่าที่สุด หากต้องการดึงสแนปช็อตที่เก่ากว่าหนึ่งรายการออกมาเป็นไบต์เดี่ยว เช่น เพื่อเปรียบเทียบความแตกต่างของสิ่งที่การแก้ไขเปลี่ยนไป ให้เรียกตัวดึงโดยตรง:

examples/extract-revision.php
<?php
declare(strict_types=1);
namespace App\Pdf\Diagnostics;
use NextPDF\Artisan\Exception\PdfParseException;
use NextPDF\Parser\PdfReader;
use NextPDF\Parser\RevisionExtractor;
/**
* Extract one revision of a PDF as standalone bytes.
*
* @throws PdfParseException If the file is unreadable or the revision index is out of range.
*/
function extractRevision(string $pdfData, int $revision): string
{
if ($pdfData === '') {
throw new PdfParseException('Empty PDF input');
}
$reader = new PdfReader($pdfData);
$reader->parse();
// Throws PdfParseException with an "out of range" message for an invalid index.
return RevisionExtractor::extractRevision($pdfData, $reader, $revision);
}

ความล้มเหลวของตัวแยกวิเคราะห์ทุกครั้งจะปรากฏเป็น NextPDF\Artisan\Exception\PdfParseException ข้อความจะระบุสาเหตุ ใช้ตารางด้านล่างเพื่อจับคู่ส่วนย่อยของข้อความกับขั้นตอนที่ทำให้เกิดขึ้น

ส่วนย่อยของข้อความขั้นตอนความหมาย
missing %PDF- headerPdfReader::parse()ไบต์เหล่านี้ไม่ใช่ PDF หรืออินพุตถูกตัดทอนที่ตอนต้น
Cannot find startxref marker / Invalid startxref offsetPdfReader::parse()ส่วนท้ายของไฟล์เสียหาย หรือพอยน์เตอร์การอ้างอิงไขว้อยู่นอกขอบเขต
Expected 'xref' keyword / Invalid xref subsection headerCrossRefParser::parseXRefTable()ตารางอ้างอิงไขว้แบบดั้งเดิมผิดรูปแบบ
XRef stream ... /Type /XRef / invalid /W arrayCrossRefParser::parseXRefStream()สตรีมอ้างอิงไขว้ขาดรายการ dictionary ที่จำเป็น
exceeds limit of (จำนวน xref หรือ object-stream)CrossRefParser / PdfReaderจำนวนที่ถูกปลอมแปลงไปสะดุดการป้องกันการปฏิเสธบริการ
Unsupported stream filterStreamDecoder::decode()สตรีมใช้ตัวกรองที่อยู่นอกชุดที่รองรับ FlateDecode / ASCIIHexDecode / ASCII85Decode
FlateDecode decompression failed / output exceeds ... bytes limitStreamDecoderข้อมูลที่บีบอัดไม่ถูกต้องหรือขยายเกินเพดาน 16 MiB
Maximum nesting depth ... exceeded / Keyword exceeds maximum lengthPdfTokenizerโครงสร้างที่ถูกประดิษฐ์ขึ้นหรือผิดปกติไปสะดุดขีดจำกัดของตัวแบ่งโทเคน
Page index ... not found / out of range in subtreePdfReader::getPage()ดัชนีหน้าที่ร้องขอไม่มีอยู่ใน page tree
Revision index ... out of rangePdfReader / RevisionExtractorดัชนีรีวิชันอยู่นอกช่วง 0 ถึง getRevisionCount() - 1

เมื่อคุณจับข้อยกเว้น ให้บันทึกข้อความและพาธต้นทาง แล้วจึงโยนซ้ำหรือคืนค่าข้อผิดพลาดที่กำหนดไว้ อย่าทิ้งข้อยกเว้นนั้นไปอย่างเงียบๆ บล็อก catch ที่ว่างเปล่าจะซ่อนข้อมูลสำคัญที่ตัวแยกวิเคราะห์สร้างไว้ให้

examples/parse-with-diagnostics.php
<?php
declare(strict_types=1);
namespace App\Pdf\Diagnostics;
use NextPDF\Artisan\Exception\PdfParseException;
use NextPDF\Parser\PdfReader;
use Psr\Log\LoggerInterface;
/**
* Parse a PDF, logging the precise parser-stage message on failure.
*
* @throws PdfParseException Rethrown after logging so the caller can decide policy.
*/
function parseWithDiagnostics(string $pdfData, LoggerInterface $logger): PdfReader
{
if ($pdfData === '') {
throw new PdfParseException('Empty PDF input');
}
$reader = new PdfReader($pdfData);
try {
$reader->parse();
} catch (PdfParseException $exception) {
$logger->error('PDF parse failed', [
'reason' => $exception->getMessage(),
'bytes' => \strlen($pdfData),
]);
throw $exception;
}
return $reader;
}
  • เรียก parse() ก่อนเสมอ ตัวเข้าถึงทุกตัวบน PdfReader สันนิษฐานว่าห่วงโซ่การอ้างอิงไขว้ถูกโหลดแล้ว การเรียก getObject() หรือ getPage() ก่อน parse() จะคืนค่าที่ไม่มีประโยชน์
  • ปฏิบัติต่อตัวแยกวิเคราะห์ในฐานะแบบอ่านอย่างเดียวและถูกกำหนดรูปโดย Chrome มันมุ่งเป้าไปที่ชุดย่อยของไวยากรณ์ PDF ที่ printToPDF ของ Chrome สร้างขึ้น PDF ที่เข้ารหัสลับ ตาราง hint แบบ linearized และการอัปเดตทีละส่วนที่ขัดแย้งกันอยู่นอกขอบเขตโดยการออกแบบ อย่าขยายมันให้กลายเป็นเครื่องมือซ่อม PDF เอนกประสงค์
  • คงขีดจำกัดด้านความปลอดภัยไว้ เพดานการซ้อนกัน ความยาวคีย์เวิร์ด ขนาด array จำนวนการอ้างอิงไขว้ และการคลายการบีบอัด จำกัดการใช้ทรัพยากรกับอินพุตที่เป็นอันตราย PdfParseException ที่มาจากขีดจำกัดคือผลลัพธ์ที่ถูกต้องสำหรับไฟล์ที่ถูกประดิษฐ์ขึ้น การยกระดับขีดจำกัดเพื่อยอมรับไฟล์เช่นนั้นจะเป็นการขยายพื้นผิวการโจมตี
  • ตั้งค่าเริ่มต้นเป็นหน้า 0 getPage() และ PageImporter::import() ตั้งค่าเริ่มต้นเป็นหน้าแรก เลือกดัชนีอื่นเฉพาะเมื่อเวิร์กโฟลว์จำเป็นต้องใช้โดยตั้งใจ
  • ตรวจสอบความถูกต้องของอินพุตก่อนสร้างตัวอ่าน ปฏิเสธไบต์ที่ว่างเปล่าหรืออ่านไม่ได้ตั้งแต่เนิ่นๆ เช่นเดียวกับตัวอย่างข้างต้น เพื่อให้ข้อผิดพลาดระดับแอปพลิเคชันที่ชัดเจนปรากฏก่อนข้อยกเว้นใดๆของตัวแยกวิเคราะห์
  • จับ PdfParseException ไม่ใช่ \Exception เปล่าๆ เป็นชนิดเดียวที่เฉพาะเจาะจงซึ่งตัวแยกวิเคราะห์โยน การจับชนิดนี้ช่วยป้องกันไม่ให้ความล้มเหลวที่ไม่เกี่ยวข้องถูกบดบัง
  • คู่มือนักพัฒนาของ Artisan — ขอบเขตการนำเข้าที่อยู่เหนือตัวแยกวิเคราะห์ รวมถึง ChromeHtmlRenderer, PageImporter และเลเยอร์สถาปัตยกรรม
  • เอกสารอ้างอิง API ของ Artisan — ตารางเมธอดที่เผยแพร่สำหรับพื้นผิวสาธารณะของแพ็กเกจ
  • การแก้ไขปัญหาของ Artisan — แนวทางแบบเริ่มจากอาการสำหรับความล้มเหลวของเรนเดอเรอร์และการนำเข้า
  • การตั้งค่าเรนเดอเรอร์ Chrome — การกำหนดค่าเรนเดอเรอร์ที่สร้าง PDF ที่ตัวแยกวิเคราะห์นี้อ่าน
  • ISO 32000-2:2020 §7.5 (โครงสร้างไฟล์ การอ้างอิงไขว้ การอัปเดตทีละส่วน) และ §7.2 (ข้อกำหนดเชิงคำศัพท์) — ข้อกำหนดที่ตัวแบ่งโทเคนและตัวแยกวิเคราะห์การอ้างอิงไขว้นำมาใช้ โปรดศึกษามาตรฐานที่เผยแพร่เพื่อดูรูปแบบระดับไบต์ที่เป็นแหล่งอ้างอิงที่เชื่อถือได้