Diagnostik parser PDF tingkat lanjut
Sekilas pandang
Bagian berjudul “Sekilas pandang”Jalur impor Artisan membaca berkas Portable Document Format (PDF) yang dihasilkan Chrome dan memasukkan satu halaman ke dalam dokumen NextPDF. Ketika masukan yang sulit membuat impor gagal, telusuri dari PageImporter::import() hingga ke kelas parser yang membaca berkas bita demi bita.
Panduan ini membahas antarmuka parser tingkat rendah di namespace NextPDF\Parser: PdfReader, PdfTokenizer, CrossRefParser, StreamDecoder, ResourceCollector, RevisionExtractor, serta objek nilai PdfObject dan RevisionXRefTable. Setiap simbol yang ditampilkan di sini tersedia dalam nextpdf/artisan. Panduan ini menjelaskan parser sebagaimana dibangun saat ini, bukan antarmuka yang ideal.
Gunakan panduan ini sebagai penjelasan sekaligus petunjuk praktis. Panduan ini menunjukkan bagaimana setiap bagian saling terhubung, lalu memandu Anda memeriksa revisi pembaruan inkremental. Untuk batas impor di atas lapisan ini, lihat panduan pengembang Artisan.
Kapan Anda memerlukan ini
Bagian berjudul “Kapan Anda memerlukan ini”Gunakan antarmuka parser hanya ketika jalur impor normal gagal dan Anda perlu menemukan penyebabnya. Pemicu umum meliputi:
PageImporter::import()melemparNextPDF\Artisan\Exception\PdfParseException, dan Anda perlu mengetahui apakah penyebabnya tabel referensi silang, filter aliran, atau pohon halaman.- Pemutakhiran Chrome mengubah format keluaran, misalnya ketika tabel referensi silang tradisional berubah menjadi aliran referensi silang, atau sebaliknya, sehingga fixture Anda tidak lagi cocok.
- Anda menerima PDF pihak ketiga yang tidak dihasilkan Chrome dan ingin memastikan apakah parser mampu membacanya sama sekali.
- Anda menganalisis dokumen yang diperbarui secara inkremental dan memerlukan rentang bita per revisi atau visibilitas objek.
Jika Anda menulis integrasi renderer biasa, Anda tidak memerlukan antarmuka ini. Parser ini adalah alat diagnostik internal, bukan pustaka PDF serbaguna. Alat ini tidak mendukung PDF terenkripsi, tabel petunjuk terlinearisasi, atau pembaruan inkremental dengan pendefinisian ulang objek yang saling bertentangan.
Antarmuka parser
Bagian berjudul “Antarmuka parser”Parser ini terdiri dari sekumpulan kecil kelas dengan tanggung jawab tunggal. PdfReader adalah titik masuknya. Kelas-kelas lainnya adalah kolaborator yang dibuat atau dipanggil olehnya.
| Kelas | Tanggung jawab | Metode utama |
|---|---|---|
PdfReader | Membaca struktur berkas, meresolusi objek, dan menelusuri pohon halaman. | parse(), getObject(), getTrailer(), getObjectNumbers(), getPage(), getPageContentStream(), getPageResources(), getPageMediaBox(), resolveRef(), collectPageResources(), getRevisionCount(), getRevisionXRef(), getRevisions() |
PdfTokenizer | Menganalisis sintaks leksikal sesuai ISO 32000-2:2020 §7.2: nama, string, angka, kamus, larik, dan referensi. | readToken(), readValue(), readName(), readNumber(), readDictionary(), readArray(), readStreamData(), peek(), skipWhitespace(), getOffset(), setOffset() |
CrossRefParser | Mengurai tabel referensi silang tradisional dan aliran referensi silang. | parseXRefTable(), parseXRefStream() |
StreamDecoder | Mendekode bita aliran berdasarkan /Filter. | decode() (statis) |
ResourceCollector | Menelusuri pohon Resources secara rekursif dan mengumpulkan setiap objek tak langsung yang dapat dijangkau. | traverse(), getCollected() |
RevisionExtractor | Memotong berkas yang diperbarui secara inkremental menjadi rentang bita per revisi. | extractRevision() (statis), getRevisionBoundaries() (statis) |
PdfObject | Objek tak langsung terurai yang tak dapat diubah (kamus beserta aliran opsional). | get(), getRef(), getArray(), getType(), getSubtype(), hasStream(), getDictionary(), getRawStreamData(), getRawDictionaryBytes() |
RevisionXRefTable | Snapshot referensi silang per revisi yang tak dapat diubah. | getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize() |
PdfReader — titik masuk
Bagian berjudul “PdfReader — titik masuk”Bentuk \NextPDF\Parser\PdfReader dengan bita PDF mentah, lalu panggil parse() sebelum memanggil metode lainnya. parse() memeriksa header %PDF-, menemukan startxref di akhir berkas, dan menelusuri rantai referensi silang dengan mengikuti tautan /Prev.
Setelah parse(), pembaca menyediakan tiga kelompok metode:
- Akses objek.
getObject(int $objNum)mengembalikan sebuahPdfObject, yang secara otomatis meresolusi entri Type 2 (objek yang disimpan di dalam aliran objek).getObjectNumbers()mengembalikanlist<int>terurut dari setiap nomor objek yang tidak bebas.resolveRef(mixed $value)mengikuti satu referensi tak langsung. Nilai langsung diteruskan tanpa perubahan. - Akses halaman.
getPage(int $pageIndex)meresolusi katalog, menelusuri/Pages, dan mengembalikan halaman pada indeks berbasis-nol.getPageContentStream(),getPageResources(), dangetPageMediaBox()mengekstrak bagian-bagian yang dibutuhkanPageImporter.collectPageResources()mengembalikanarray<int, PdfObject>untuk setiap objek yang dapat dijangkau dari Resources dan Contents halaman tersebut. - Akses revisi.
getRevisionCount()mengembalikan jumlah revisi inkremental. Berkas dengan satu revisi mengembalikan1.getRevisionXRef(int $index)mengembalikan satuRevisionXRefTable(indeks0adalah yang paling baru).getRevisions()mengembalikanlist<RevisionXRefTable>lengkap.
PdfTokenizer — analisis leksikal
Bagian berjudul “PdfTokenizer — analisis leksikal”PdfTokenizer membaca aliran bita. Anda jarang perlu membentuknya sendiri karena PdfReader dan CrossRefParser memiliki instance masing-masing. Periksa lapisan ini ketika penguraian gagal pada token yang cacat. Dua perilaku penting untuk diagnostik:
- Batas keamanan adalah konstanta, bukan konfigurasi. Tokenizer membatasi penyarangan string literal, penyarangan kamus dan larik, panjang kata kunci, serta jumlah elemen larik. Ketika masukan melampaui suatu batas, tokenizer melempar
PdfParseExceptiondan menyebutkan batas tersebut dalam pesannya. Masukan yang dirancang khusus untuk memicu salah satu batas ini adalah pertahanan yang bekerja sesuai rancangan, bukan bug parser. readValue()mengarahkan penguraian. Metode ini memeriksa bita berikutnya dan mendelegasikannya kereadName(),readLiteralString(),readHexString(),readArray(),readDictionary(), atau pembaca number/reference. Referensi tak langsungN G Rdikembalikan sebagai bentuk larik['type' => 'ref', 'num' => N, 'gen' => G].PdfObject::getRef()danPdfReader::resolveRef()mengenali bentuk ini.
CrossRefParser — resolusi referensi silang
Bagian berjudul “CrossRefParser — resolusi referensi silang”CrossRefParser mengurai dua format yang dapat dihasilkan Chrome:
parseXRefTable()membaca tabelxreftradisional (gaya PDF 1.x): header subbagian, entri 20 bita dengan lebar tetap, lalu kamustrailer.parseXRefStream()membaca aliran referensi silang (PDF 2.0, ISO 32000-2:2020 §7.5.8): objek tak langsung dengan/Type /XRef, larik lebar-bidang/W, dan aliran biner berisi entri.
Keduanya mengembalikan bentuk yang sama: array{xref: array<int, ...>, trailer: array<string, mixed>, prevOffset: int|null}. PdfReader::parse() menentukan parser mana yang dipanggil dengan mengintip empat bita pada offset referensi silang: xref memilih parser tabel, sedangkan nilai selain itu diperlakukan sebagai objek aliran. Kedua parser memberlakukan batas atas satu juta entri per subbagian untuk menolak hitungan palsu yang, jika tidak dibatasi, akan membuat parser bekerja berlebihan.
StreamDecoder — filter aliran
Bagian berjudul “StreamDecoder — filter aliran”StreamDecoder::decode(string $data, string|array $filter) bersifat statis dan menerapkan satu filter atau daftar filter berantai. Metode ini hanya mendukung filter yang dihasilkan printToPDF Chrome:
FlateDecode(zlib, dengan fallback raw-deflate)ASCIIHexDecodeASCII85Decode
Nama filter lainnya melempar PdfParseException dengan Unsupported stream filter. Dekoder membatasi keluaran terdekompresi hingga 16 MiB untuk mengurangi risiko bom dekompresi. Keluaran yang terlalu besar melempar pengecualian alih-alih mengalokasikan memori tanpa batas. Ketika PdfReader membaca aliran dan proses dekode melempar pengecualian, pembaca kembali ke bita aliran mentah, sehingga satu filter yang buruk tidak menggagalkan seluruh penguraian.
ResourceCollector — penelusuran sumber daya mendalam
Bagian berjudul “ResourceCollector — penelusuran sumber daya mendalam”ResourceCollector dibentuk dengan PdfReader dan dipanggil melalui PdfReader::collectPageResources(). Metode traverse() menelusuri sebuah nilai secara rekursif, mengikuti setiap referensi ['type' => 'ref'] melalui getObject(), dan mencatat setiap objek yang teresolusi satu kali dalam array<int, PdfObject> yang dikunci dengan nomor objek. Metode ini membatasi kedalaman rekursi dan diam-diam melewati referensi yang tidak dapat diresolusinya, sehingga satu referensi yang menggantung menghasilkan koleksi parsial alih-alih kegagalan total.
RevisionExtractor — pembaruan inkremental dan revisi
Bagian berjudul “RevisionExtractor — pembaruan inkremental dan revisi”PDF yang ditandatangani, dianotasi, atau disunting dengan cara lain setelah dibuat akan membawa pembaruan inkremental. Setiap suntingan menambahkan bagian referensi silang dan trailer baru yang diakhiri dengan penanda %%EOF. RevisionExtractor bekerja sepenuhnya melalui metode statis pada PdfReader yang sudah diurai:
extractRevision(string $pdfData, PdfReader $reader, int $revision)mengembalikan berkas yang dipotong pada batas%%EOFdari revisi yang diminta. Revisi0(paling baru) mengembalikan seluruh berkas; indeks yang lebih tinggi mengembalikan snapshot yang lebih lama.getRevisionBoundaries(string $pdfData, PdfReader $reader)mengembalikanlist<array{revision, startByte, endByte, sizeBytes}>yang menjelaskan rentang bita yang disumbangkan setiap revisi.
Isolasi ini disengaja. Mengekstrak revisi yang lebih lama hanya memunculkan objek yang terlihat hingga titik tersebut, sehingga memblokir serangan referensi silang hibrida ketika revisi yang lebih baru mendefinisikan ulang objek yang lebih awal.
Panduan langkah demi langkah: memeriksa sebuah revisi
Bagian berjudul “Panduan langkah demi langkah: memeriksa sebuah revisi”Prosedur ini memeriksa riwayat revisi sebuah PDF yang mungkin telah disunting setelah dihasilkan oleh Chrome. Contoh ini dirancang untuk produksi: mendeklarasikan tipe ketat, menggunakan petunjuk tipe lengkap, memvalidasi masukan, dan menangkap pengecualian yang paling spesifik.
- Baca bita PDF ke dalam memori dan tolak masukan kosong sebelum membentuk pembaca.
- Bentuk
\NextPDF\Parser\PdfReaderdan panggilparse(). - Baca
getRevisionCount(). Nilai1berarti berkas memiliki satu revisi tanpa pembaruan inkremental. - Untuk setiap revisi, baca
RevisionXRefTable-nya dan periksagetActiveObjectCount(),hasRootUpdate(), dangetSize(). - Hitung rentang bita per revisi dengan
RevisionExtractor::getRevisionBoundaries(). - Tangkap
PdfParseException, pengecualian paling spesifik yang dilempar parser, dan tampilkan pesan diagnostik.
<?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;}Pembaca mengurutkan revisi dari yang terbaru (index0) hingga yang terlama. Untuk mengekstrak satu snapshot yang lebih lama sebagai bita mandiri, misalnya untuk membandingkan perubahan dari suatu suntingan, panggil ekstraktor secara langsung:
<?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);}Penanganan kegagalan
Bagian berjudul “Penanganan kegagalan”Setiap kegagalan parser muncul sebagai NextPDF\Artisan\Exception\PdfParseException. Pesannya mengidentifikasi penyebab kegagalan. Gunakan tabel di bawah ini untuk memetakan fragmen pesan ke tahap yang memunculkannya.
| Fragmen pesan | Tahap | Apa artinya |
|---|---|---|
missing %PDF- header | PdfReader::parse() | Bita tersebut bukan PDF, atau masukan terpotong di awal. |
Cannot find startxref marker / Invalid startxref offset | PdfReader::parse() | Akhir berkas rusak, atau penunjuk referensi silang berada di luar batas. |
Expected 'xref' keyword / Invalid xref subsection header | CrossRefParser::parseXRefTable() | Tabel referensi silang tradisional cacat. |
XRef stream ... /Type /XRef / invalid /W array | CrossRefParser::parseXRefStream() | Aliran referensi silang kehilangan entri kamus yang diperlukan. |
exceeds limit of (jumlah xref atau object-stream) | CrossRefParser / PdfReader | Hitungan palsu memicu pelindung denial-of-service. |
Unsupported stream filter | StreamDecoder::decode() | Aliran menggunakan filter di luar set FlateDecode / ASCIIHexDecode / ASCII85Decode yang didukung. |
FlateDecode decompression failed / output exceeds ... bytes limit | StreamDecoder | Data terkompresi tidak valid atau membesar melampaui batas 16 MiB. |
Maximum nesting depth ... exceeded / Keyword exceeds maximum length | PdfTokenizer | Struktur yang dirancang khusus atau patologis memicu batas tokenizer. |
Page index ... not found / out of range in subtree | PdfReader::getPage() | Indeks halaman yang diminta tidak ada di pohon halaman. |
Revision index ... out of range | PdfReader / RevisionExtractor | Indeks revisi berada di luar rentang 0 hingga getRevisionCount() - 1. |
Ketika Anda menangkap pengecualian, catat pesan dan jalur sumbernya, lalu lempar ulang atau kembalikan kesalahan yang terdefinisi. Jangan abaikan secara diam-diam. Blok catch yang kosong menyembunyikan satu-satunya informasi yang sudah dihasilkan parser.
<?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;}Nilai standar yang aman
Bagian berjudul “Nilai standar yang aman”- Selalu panggil
parse()terlebih dahulu. Setiap aksesor padaPdfReadermengasumsikan rantai referensi silang telah dimuat. MemanggilgetObject()ataugetPage()sebelumparse()tidak menghasilkan apa pun yang berguna. - Perlakukan parser sebagai hanya-baca dan berbentuk Chrome. Parser ini menargetkan bagian sintaks PDF yang dihasilkan
printToPDFChrome. PDF terenkripsi, tabel petunjuk terlinearisasi, dan pembaruan inkremental yang saling bertentangan berada di luar cakupan secara rancangan. Jangan kembangkan menjadi alat perbaikan PDF umum. - Pertahankan batas keamanan tetap berlaku. Batas penyarangan, panjang kata kunci, ukuran larik, jumlah referensi silang, dan dekompresi membatasi penggunaan sumber daya pada masukan yang berbahaya.
PdfParseExceptionakibat suatu batas adalah hasil yang benar untuk berkas yang dirancang khusus. Menaikkan batas untuk menerima berkas semacam itu memperluas permukaan serangan. - Gunakan halaman
0sebagai standar.getPage()danPageImporter::import()menggunakan halaman pertama sebagai standar. Pilih indeks lain hanya ketika alur kerja memang membutuhkannya. - Validasi masukan sebelum membentuk pembaca. Tolak bita kosong atau tak terbaca sejak dini, seperti yang dilakukan contoh di atas, sehingga kesalahan tingkat aplikasi yang jelas muncul sebelum pengecualian parser apa pun.
- Tangkap
PdfParseException, jangan pernah\Exceptionmentah. Itu adalah satu-satunya tipe spesifik yang dilempar parser. Menangkapnya mencegah kegagalan yang tidak terkait dari tertutupi.
Lihat juga
Bagian berjudul “Lihat juga”- Panduan pengembang Artisan — batas impor di atas parser, termasuk
ChromeHtmlRenderer,PageImporter, dan lapisan arsitektur. - Referensi API Artisan — tabel metode terpublikasi untuk antarmuka publik paket.
- Pemecahan masalah Artisan — panduan berbasis gejala untuk kegagalan renderer dan impor.
- Penyiapan renderer Chrome — mengonfigurasi renderer yang menghasilkan PDF yang dibaca parser ini.
- ISO 32000-2:2020 §7.5 (struktur berkas, referensi silang, pembaruan inkremental) dan §7.2 (konvensi leksikal) — spesifikasi yang diimplementasikan oleh tokenizer dan parser referensi silang. Rujuk standar yang dipublikasikan untuk format tingkat-bita yang otoritatif.