Geavanceerde diagnostiek voor de PDF-parser
In het kort
Sectie met titel “In het kort”Het importpad van Artisan leest een door Chrome gegenereerd Portable Document Format-bestand (PDF) en importeert één pagina in een NextPDF-document. Als lastige invoer die import laat mislukken, kijk je onder PageImporter::import() naar de parserklassen die het bestand byte voor byte lezen.
Deze gids behandelt de low-level parserlaag in de namespace NextPDF\Parser: PdfReader, PdfTokenizer, CrossRefParser, StreamDecoder, ResourceCollector, RevisionExtractor, en de value objects PdfObject en RevisionXRefTable. Elk symbool dat hier wordt getoond, bestaat in nextpdf/artisan. De gids beschrijft de parser zoals die is gebouwd, niet als een geïdealiseerde interface.
Gebruik deze gids zowel als uitleg als als handleiding. Je ziet hoe de onderdelen samenhangen en loopt daarna door het inspecteren van een revisie bij een incrementele update. Voor de importgrens boven deze laag raadpleeg je de ontwikkelaarsgids voor Artisan.
Wanneer je dit nodig hebt
Sectie met titel “Wanneer je dit nodig hebt”Gebruik het parseroppervlak alleen wanneer het normale importpad al is mislukt en je de oorzaak moet vinden. Typische aanleidingen zijn onder meer:
PageImporter::import()gooitNextPDF\Artisan\Exception\PdfParseException, en je moet weten of de kruisverwijzingstabel, een streamfilter of de paginaboom de oorzaak is.- Een upgrade van Chrome verandert het uitvoerformaat, bijvoorbeeld wanneer een traditionele kruisverwijzingstabel een kruisverwijzingsstream wordt, of andersom, en je fixtures niet meer overeenkomen.
- Je ontvangt een PDF van een derde partij die niet door Chrome is geproduceerd en wilt bevestigen of de parser deze überhaupt kan lezen.
- Je analyseert een incrementeel bijgewerkt document en hebt byte-bereiken per revisie of inzicht in de zichtbaarheid van objecten nodig.
Als je een normale renderer-integratie schrijft, heb je dit oppervlak niet nodig. De parser is een intern diagnostisch hulpmiddel, geen algemene PDF-bibliotheek. Hij ondersteunt geen versleutelde PDF-bestanden, gelineariseerde hinttabellen of incrementele updates met conflicterende herdefinities van objecten.
Parseroppervlak
Sectie met titel “Parseroppervlak”De parser bestaat uit een kleine verzameling klassen met elk één verantwoordelijkheid. PdfReader is het ingangspunt. De andere klassen zijn samenwerkende objecten die PdfReader construeert of aanroept.
| Klasse | Verantwoordelijkheid | Belangrijkste methoden |
|---|---|---|
PdfReader | Leest de bestandsstructuur, lost objecten op en doorloopt de paginaboom. | parse(), getObject(), getTrailer(), getObjectNumbers(), getPage(), getPageContentStream(), getPageResources(), getPageMediaBox(), resolveRef(), collectPageResources(), getRevisionCount(), getRevisionXRef(), getRevisions() |
PdfTokenizer | Analyseert de lexicale syntaxis volgens ISO 32000-2:2020 §7.2: namen, strings, getallen, dictionaries, arrays en verwijzingen. | readToken(), readValue(), readName(), readNumber(), readDictionary(), readArray(), readStreamData(), peek(), skipWhitespace(), getOffset(), setOffset() |
CrossRefParser | Parseert traditionele kruisverwijzingstabellen en kruisverwijzingsstreams. | parseXRefTable(), parseXRefStream() |
StreamDecoder | Decodeert streambytes per /Filter. | decode() (statisch) |
ResourceCollector | Doorloopt een Resources-boom recursief en verzamelt elk bereikbaar indirect object. | traverse(), getCollected() |
RevisionExtractor | Splitst een incrementeel bijgewerkt bestand op in byte-bereiken per revisie. | extractRevision() (statisch), getRevisionBoundaries() (statisch) |
PdfObject | Onveranderlijk geparseerd indirect object (dictionary plus optionele stream). | get(), getRef(), getArray(), getType(), getSubtype(), hasStream(), getDictionary(), getRawStreamData(), getRawDictionaryBytes() |
RevisionXRefTable | Onveranderlijke kruisverwijzingsmomentopname per revisie. | getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize() |
PdfReader — het ingangspunt
Sectie met titel “PdfReader — het ingangspunt”Maak \NextPDF\Parser\PdfReader aan met de ruwe PDF-bytes en roep daarna parse() aan voordat je een andere methode aanroept. parse() controleert de %PDF--header, zoekt startxref aan het einde van het bestand en doorloopt de kruisverwijzingsketen door de /Prev-koppelingen te volgen.
Na parse() biedt de reader drie groepen methoden:
- Objecttoegang.
getObject(int $objNum)retourneert eenPdfObjecten lost Type 2-entries (objecten die in een object-stream zijn opgeslagen) automatisch op.getObjectNumbers()retourneert een gesorteerdelist<int>met elk niet-vrij objectnummer.resolveRef(mixed $value)volgt één indirecte verwijzing. Een directe waarde blijft ongewijzigd. - Paginatoegang.
getPage(int $pageIndex)lost de catalogus op, doorloopt/Pagesen retourneert de pagina met de op nul gebaseerde index.getPageContentStream(),getPageResources()engetPageMediaBox()extraheren de onderdelen diePageImporternodig heeft.collectPageResources()retourneertarray<int, PdfObject>met elk object dat bereikbaar is vanuit de Resources en Contents van de pagina. - Revisietoegang.
getRevisionCount()retourneert het aantal incrementele revisies. Een bestand met één revisie retourneert1.getRevisionXRef(int $index)retourneert éénRevisionXRefTable(index0is de meest recente).getRevisions()retourneert de volledigelist<RevisionXRefTable>.
PdfTokenizer — lexicale analyse
Sectie met titel “PdfTokenizer — lexicale analyse”PdfTokenizer leest de bytestream. Je maakt hem zelden zelf aan, omdat PdfReader en CrossRefParser hun eigen instanties beheren. Inspecteer deze laag wanneer een parse mislukt op een misvormd token. Twee eigenschappen zijn belangrijk voor diagnostiek:
- Beveiligingslimieten zijn constanten, geen configuratie. De tokenizer begrenst de nesting van literal strings, de nesting van dictionaries en arrays, de lengte van keywords en het aantal array-elementen. Als invoer een limiet overschrijdt, gooit de tokenizer
PdfParseExceptionen noemt hij de limiet in de melding. Geprepareerde invoer die een van deze limieten activeert, is een verdediging die werkt zoals bedoeld, geen parserbug. readValue()stuurt de parse aan. De methode inspecteert de volgende byte en delegeert naarreadName(),readLiteralString(),readHexString(),readArray(),readDictionary(), of een number/reference-reader. Een indirecte verwijzingN G Rwordt geretourneerd als de array-vorm['type' => 'ref', 'num' => N, 'gen' => G].PdfObject::getRef()enPdfReader::resolveRef()herkennen deze vorm.
CrossRefParser — kruisverwijzingsresolutie
Sectie met titel “CrossRefParser — kruisverwijzingsresolutie”CrossRefParser parseert beide formaten die Chrome kan genereren:
parseXRefTable()leest een traditionelexref-tabel (PDF 1.x-stijl): subsectie-headers, entries met vaste breedte van 20 bytes en vervolgens eentrailer-dictionary.parseXRefStream()leest een kruisverwijzingsstream (PDF 2.0, ISO 32000-2:2020 §7.5.8): een indirect object met/Type /XRef, een/W-array met veldbreedtes en een binaire stream met entries.
Beide retourneren dezelfde vorm: array{xref: array<int, ...>, trailer: array<string, mixed>, prevOffset: int|null}. PdfReader::parse() bepaalt welke parser wordt aangeroepen door te kijken naar de vier bytes vanaf de kruisverwijzingsoffset: xref selecteert de tabelparser en al het andere wordt als stream-object behandeld. Beide parsers handhaven een plafond van één miljoen entries per subsectie om vervalste tellingen af te wijzen die de parser anders buitensporig lang zouden laten lopen.
StreamDecoder — streamfilters
Sectie met titel “StreamDecoder — streamfilters”StreamDecoder::decode(string $data, string|array $filter) is statisch en past één filter of een geketende lijst filters toe. Hij ondersteunt precies de filters die Chrome via printToPDF produceert:
FlateDecode(zlib, met een raw-deflate-fallback)ASCIIHexDecodeASCII85Decode
Bij elke andere filternaam gooit de decoder PdfParseException met Unsupported stream filter. De decoder begrenst de gedecomprimeerde uitvoer op 16 MiB om het risico van een decompressiebom te beperken. Te grote uitvoer gooit een exception in plaats van onbeperkt geheugen te alloceren. Wanneer PdfReader een stream leest en het decoderen een exception gooit, valt hij terug op de ruwe streambytes, zodat één slecht filter niet de hele parse afbreekt.
ResourceCollector — diepe resourcetraversal
Sectie met titel “ResourceCollector — diepe resourcetraversal”ResourceCollector wordt geconstrueerd met de PdfReader en aangeroepen via PdfReader::collectPageResources(). De methode traverse() doorloopt een waarde recursief, volgt elke ['type' => 'ref']-verwijzing via getObject() en registreert elk opgelost object eenmaal in een array<int, PdfObject> met het objectnummer als sleutel. Hij begrenst de recursiediepte en slaat verwijzingen die hij niet kan oplossen stilzwijgend over, zodat één losse verwijzing een gedeeltelijke verzameling oplevert in plaats van een harde fout.
RevisionExtractor — incrementele updates en revisies
Sectie met titel “RevisionExtractor — incrementele updates en revisies”Een PDF die na het aanmaken is ondertekend, geannoteerd of anderszins bewerkt, bevat incrementele updates. Elke bewerking voegt een nieuwe kruisverwijzingssectie en trailer toe en eindigt met een %%EOF-marker. RevisionExtractor werkt uitsluitend met statische methoden op basis van een geparseerde PdfReader:
extractRevision(string $pdfData, PdfReader $reader, int $revision)retourneert het bestand afgekapt op de%%EOF-grens van de gevraagde revisie. Revisie0(meest recent) retourneert het hele bestand; hogere indices retourneren steeds oudere momentopnamen.getRevisionBoundaries(string $pdfData, PdfReader $reader)retourneert eenlist<array{revision, startByte, endByte, sizeBytes}>die het byte-bereik beschrijft dat elke revisie heeft bijgedragen.
Die isolatie is bewust gekozen. Het extraheren van een oudere revisie onthult alleen de objecten die tot dat punt zichtbaar zijn en blokkeert zo hybride kruisverwijzingsaanvallen waarbij een latere revisie een eerder object herdefinieert.
Stappenplan: een revisie inspecteren
Sectie met titel “Stappenplan: een revisie inspecteren”Met deze procedure inspecteer je de revisiegeschiedenis van een PDF die mogelijk is bewerkt nadat Chrome hem heeft geproduceerd. Het voorbeeld is opgezet voor productiegebruik: het declareert strict types, gebruikt volledige type hints, valideert zijn invoer en vangt de meest specifieke exception op.
- Lees de PDF-bytes in het geheugen en wijs lege invoer af voordat je de reader construeert.
- Construeer
\NextPDF\Parser\PdfReaderen roepparse()aan. - Lees
getRevisionCount(). Een waarde van1betekent een bestand met één revisie zonder incrementele updates. - Lees voor elke revisie de
RevisionXRefTableen inspecteergetActiveObjectCount(),hasRootUpdate()engetSize(). - Bereken byte-bereiken per revisie met
RevisionExtractor::getRevisionBoundaries(). - Vang
PdfParseExceptionop, de meest specifieke exception die de parser gooit, en presenteer een diagnostische melding.
<?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;}De reader ordent revisies van de nieuwste (index0) naar de oudste. Om één oudere momentopname als losstaande bytes te extraheren, bijvoorbeeld om te diffen wat een bewerking heeft gewijzigd, roep je de extractor rechtstreeks aan:
<?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);}Foutafhandeling
Sectie met titel “Foutafhandeling”Elke parserfout wordt gemeld als NextPDF\Artisan\Exception\PdfParseException. De melding identificeert de oorzaak. Gebruik de onderstaande tabel om een meldingsfragment toe te wijzen aan de fase die het heeft veroorzaakt.
| Meldingsfragment | Fase | Wat het betekent |
|---|---|---|
missing %PDF- header | PdfReader::parse() | De bytes zijn geen PDF, of de invoer is aan het begin afgekapt. |
Cannot find startxref marker / Invalid startxref offset | PdfReader::parse() | Het einde van het bestand is corrupt, of de kruisverwijzingspointer ligt buiten de grenzen. |
Expected 'xref' keyword / Invalid xref subsection header | CrossRefParser::parseXRefTable() | Een traditionele kruisverwijzingstabel is misvormd. |
XRef stream ... /Type /XRef / invalid /W array | CrossRefParser::parseXRefStream() | Een kruisverwijzingsstream mist vereiste dictionary-entries. |
exceeds limit of (xref- of object-stream-telling) | CrossRefParser / PdfReader | Een vervalste telling activeerde een denial-of-service-beveiliging. |
Unsupported stream filter | StreamDecoder::decode() | De stream gebruikt een filter buiten de ondersteunde set FlateDecode / ASCIIHexDecode / ASCII85Decode. |
FlateDecode decompression failed / output exceeds ... bytes limit | StreamDecoder | De gecomprimeerde data is ongeldig of de uitvoer wordt groter dan de limiet van 16 MiB. |
Maximum nesting depth ... exceeded / Keyword exceeds maximum length | PdfTokenizer | Een geprepareerde of pathologische structuur activeerde een tokenizerlimiet. |
Page index ... not found / out of range in subtree | PdfReader::getPage() | De gevraagde pagina-index bestaat niet in de paginaboom. |
Revision index ... out of range | PdfReader / RevisionExtractor | De revisie-index ligt buiten het bereik 0 tot getRevisionCount() - 1. |
Wanneer je de exception opvangt, log dan de melding en het bronpad, en gooi die vervolgens opnieuw of retourneer een gedefinieerde fout. Negeer die niet stilzwijgend. Een leeg catch-blok verbergt precies de informatie die de parser bewust heeft geproduceerd.
<?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;}Veilige standaardwaarden
Sectie met titel “Veilige standaardwaarden”- Roep altijd eerst
parse()aan. Elke accessor opPdfReadergaat ervan uit dat de kruisverwijzingsketen is geladen. Het aanroepen vangetObject()ofgetPage()vóórparse()levert geen bruikbaar resultaat op. - Behandel de parser als read-only en op Chrome afgestemd. Hij richt zich op de subset van PDF-syntaxis die Chrome via
printToPDFproduceert. Versleutelde PDF-bestanden, gelineariseerde hinttabellen en conflicterende incrementele updates vallen door het ontwerp buiten de scope. Breid de parser niet uit tot een algemeen PDF-reparatiehulpmiddel. - Houd de beveiligingslimieten op hun plaats. De limieten voor nesting, keyword-lengte, array-grootte, kruisverwijzingstelling en decompressie beperken het resourcegebruik bij vijandige invoer. Een
PdfParseExceptiondoor zo’n limiet is de juiste uitkomst voor een geprepareerd bestand. Het verhogen van een limiet om zo’n bestand te accepteren, vergroot het aanvalsoppervlak. - Gebruik standaard pagina
0.getPage()enPageImporter::import()gaan standaard uit van de eerste pagina. Kies alleen een andere index wanneer de workflow dat expliciet vereist. - Valideer invoer voordat je de reader construeert. Wijs lege of onleesbare bytes vroeg af, zoals de bovenstaande voorbeelden doen, zodat een duidelijke fout op applicatieniveau verschijnt voordat er een parser-exception ontstaat.
- Vang
PdfParseExceptionop, nooit een kale\Exception. Het is het enige, specifieke type dat de parser gooit. Door dit specifieke type op te vangen voorkom je dat ongerelateerde fouten worden gemaskeerd.
Zie ook
Sectie met titel “Zie ook”- Ontwikkelaarsgids voor Artisan — de importgrens boven de parser, inclusief
ChromeHtmlRenderer,PageImporteren de architectuurlagen. - API-referentie voor Artisan — de gepubliceerde methodetabellen voor de publieke API van het pakket.
- Probleemoplossing voor Artisan — symptoomgerichte hulp bij renderer- en importfouten.
- Chrome-rendererinstallatie — het configureren van de renderer die de PDF-bestanden produceert die deze parser leest.
- ISO 32000-2:2020 §7.5 (bestandsstructuur, kruisverwijzing, incrementele updates) en §7.2 (lexicale conventies) — de specificatie die de tokenizer en de kruisverwijzingsparser implementeren. Raadpleeg de gepubliceerde standaard voor het gezaghebbende formaat op byteniveau.