Diagnostica avanzata del parser PDF
In sintesi
Sezione intitolata “In sintesi”Il percorso di importazione di Artisan legge un file Portable Document Format (PDF) generato da Chrome e porta una pagina in un documento NextPDF. Quando l’importazione non riesce su un input ostico, occorre scendere al di sotto di PageImporter::import(), fino alle classi del parser che leggono il file byte per byte.
Questa guida documenta la superficie di basso livello del parser nel namespace NextPDF\Parser: PdfReader, PdfTokenizer, CrossRefParser, StreamDecoder, ResourceCollector, RevisionExtractor e gli oggetti valore PdfObject e RevisionXRefTable. Ogni simbolo mostrato qui esiste in nextpdf/artisan. La guida descrive il parser così com’è implementato, non una superficie idealizzata.
Leggere questa guida come una spiegazione affiancata da una procedura pratica. Illustra come si combinano i singoli elementi, quindi accompagna nell’ispezione di una revisione con aggiornamento incrementale. Per il confine di importazione al di sopra di questo livello, vedere la guida per sviluppatori di Artisan.
Quando serve
Sezione intitolata “Quando serve”Usare la superficie del parser solo quando il normale percorso di importazione è già fallito ed è necessario individuare la causa. Casi tipici:
PageImporter::import()generaNextPDF\Artisan\Exception\PdfParseExceptione occorre stabilire se la responsabilità sia della tabella dei riferimenti incrociati, di un filtro di flusso o dell’albero delle pagine.- Un aggiornamento di Chrome modifica il formato di output (una tabella di riferimenti incrociati tradizionale diventa un flusso di riferimenti incrociati, o viceversa) e le fixture non corrispondono più.
- Si riceve un PDF di terze parti non prodotto da Chrome e occorre verificare se il parser riesca effettivamente a leggerlo.
- Si esegue un’analisi forense di un documento con aggiornamento incrementale e occorrono gli intervalli di byte per revisione o la visibilità degli oggetti.
Per una normale integrazione del renderer, questa superficie non è necessaria. Il parser è uno strumento diagnostico interno, non una libreria PDF generica: non supporta i PDF cifrati, le tabelle di hint linearizzate, né gli aggiornamenti incrementali con ridefinizioni di oggetti in conflitto.
Superficie del parser
Sezione intitolata “Superficie del parser”Il parser è un insieme ristretto di classi a responsabilità singola. PdfReader è il punto di ingresso; le altre sono collaboratrici che costruisce o richiama.
| Classe | Responsabilità | Metodi principali |
|---|---|---|
PdfReader | Legge la struttura del file, risolve gli oggetti e attraversa l’albero delle pagine. | parse(), getObject(), getTrailer(), getObjectNumbers(), getPage(), getPageContentStream(), getPageResources(), getPageMediaBox(), resolveRef(), collectPageResources(), getRevisionCount(), getRevisionXRef(), getRevisions() |
PdfTokenizer | Esegue l’analisi lessicale secondo ISO 32000-2:2020 §7.2 — nomi, stringhe, numeri, dizionari, array, riferimenti. | readToken(), readValue(), readName(), readNumber(), readDictionary(), readArray(), readStreamData(), peek(), skipWhitespace(), getOffset(), setOffset() |
CrossRefParser | Analizza le tabelle tradizionali e i flussi di riferimenti incrociati. | parseXRefTable(), parseXRefStream() |
StreamDecoder | Decodifica i byte del flusso in base a /Filter. | decode() (statico) |
ResourceCollector | Attraversa in profondità un albero Resources e raccoglie ogni oggetto indiretto raggiungibile. | traverse(), getCollected() |
RevisionExtractor | Suddivide un file con aggiornamento incrementale in intervalli di byte per ciascuna revisione. | extractRevision() (statico), getRevisionBoundaries() (statico) |
PdfObject | Oggetto indiretto analizzato e immutabile (dizionario più flusso opzionale). | get(), getRef(), getArray(), getType(), getSubtype(), hasStream(), getDictionary(), getRawStreamData(), getRawDictionaryBytes() |
RevisionXRefTable | Snapshot immutabile dei riferimenti incrociati per revisione. | getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize() |
PdfReader — il punto di ingresso
Sezione intitolata “PdfReader — il punto di ingresso”Costruire \NextPDF\Parser\PdfReader con i byte grezzi del PDF, quindi chiamare parse() prima di qualsiasi altro metodo. parse() verifica l’intestazione %PDF-, individua startxref nella coda del file e percorre la catena dei riferimenti incrociati seguendo i collegamenti /Prev.
Dopo parse(), il reader espone tre gruppi di metodi:
- Accesso agli oggetti.
getObject(int $objNum)restituisce unPdfObject, risolvendo in modo trasparente le voci di tipo 2 (oggetti memorizzati all’interno di un flusso di oggetti).getObjectNumbers()restituisce unalist<int>ordinata di tutti i numeri di oggetto non liberi.resolveRef(mixed $value)segue un singolo riferimento indiretto; un valore diretto viene restituito invariato. - Accesso alle pagine.
getPage(int $pageIndex)risolve il catalogo, percorre/Pagese restituisce la pagina all’indice in base zero.getPageContentStream(),getPageResources()egetPageMediaBox()estraggono le parti di cuiPageImporterha bisogno.collectPageResources()restituiscearray<int, PdfObject>di ogni oggetto raggiungibile dalle Resources e dai Contents della pagina. - Accesso alle revisioni.
getRevisionCount()restituisce il numero di revisioni incrementali (un file a revisione singola restituisce1).getRevisionXRef(int $index)restituisce unaRevisionXRefTable(l’indice0è la più recente).getRevisions()restituisce l’interalist<RevisionXRefTable>.
PdfTokenizer — analisi lessicale
Sezione intitolata “PdfTokenizer — analisi lessicale”PdfTokenizer legge il flusso di byte. Raramente lo si costruisce direttamente — PdfReader e CrossRefParser possiedono le proprie istanze — ma è il livello da ispezionare quando l’analisi fallisce su un token non valido. Due comportamenti sono importanti per la diagnostica:
- I limiti di sicurezza sono costanti, non impostazioni configurabili. Il tokenizer limita l’annidamento delle stringhe letterali, l’annidamento di dizionari e array, la lunghezza delle parole chiave e il numero di elementi degli array. Quando un limite viene superato, genera
PdfParseExceptioncon il limite indicato nel messaggio. Un input artefatto che fa scattare uno di questi limiti indica una difesa che opera come previsto, non un bug del parser. readValue()è il dispatcher. Ispeziona il byte successivo e delega areadName(),readLiteralString(),readHexString(),readArray(),readDictionary(), o a un lettore per number/reference. Un riferimento indirettoN G Rviene restituito nella forma di array['type' => 'ref', 'num' => N, 'gen' => G]. È questa la forma riconosciuta daPdfObject::getRef()ePdfReader::resolveRef().
CrossRefParser — risoluzione dei riferimenti incrociati
Sezione intitolata “CrossRefParser — risoluzione dei riferimenti incrociati”CrossRefParser analizza entrambi i formati che Chrome può emettere:
parseXRefTable()legge una tabellaxreftradizionale (stile PDF 1.x): intestazioni di sottosezione seguite da voci a larghezza fissa di 20 byte, quindi un dizionariotrailer.parseXRefStream()legge un flusso di riferimenti incrociati (PDF 2.0, ISO 32000-2:2020 §7.5.8): un oggetto indiretto con/Type /XRef, un array di larghezze di campo/We un flusso binario di voci.
Entrambi restituiscono lo stesso formato: array{xref: array<int, ...>, trailer: array<string, mixed>, prevOffset: int|null}. PdfReader::parse() decide quale chiamare osservando i quattro byte all’offset dei riferimenti incrociati: xref seleziona il parser della tabella; qualsiasi altro valore viene trattato come oggetto di flusso. Entrambi i parser applicano un limite massimo di un milione di voci per sottosezione, per rifiutare conteggi falsificati che altrimenti costringerebbero il parser a un lavoro eccessivo.
StreamDecoder — filtri di flusso
Sezione intitolata “StreamDecoder — filtri di flusso”StreamDecoder::decode(string $data, string|array $filter) è statico e applica uno o un elenco concatenato di filtri. Supporta esattamente i filtri che printToPDF di Chrome emette:
FlateDecode(zlib, con fallback a raw-deflate)ASCIIHexDecodeASCII85Decode
Qualsiasi altro nome di filtro genera PdfParseException con Unsupported stream filter. Il decoder limita l’output decompresso a 16 MiB per contenere il rischio di decompression bomb; un output sovradimensionato genera un’eccezione anziché allocare memoria senza limiti. Quando PdfReader legge un flusso e la decodifica genera un’eccezione, ripiega sui byte grezzi del flusso, così che un singolo filtro difettoso non interrompa l’intera analisi.
ResourceCollector — attraversamento profondo delle risorse
Sezione intitolata “ResourceCollector — attraversamento profondo delle risorse”ResourceCollector viene costruito con il PdfReader e richiamato tramite PdfReader::collectPageResources(). Il suo metodo traverse() percorre ricorsivamente un valore, segue ogni riferimento ['type' => 'ref'] tramite getObject() e registra una sola volta ciascun oggetto risolto in un array<int, PdfObject> indicizzato per numero di oggetto. Limita la profondità di ricorsione e ignora silenziosamente i riferimenti che non riesce a risolvere, così che un singolo riferimento pendente produca una raccolta parziale anziché un guasto irreversibile.
RevisionExtractor — aggiornamenti incrementali e revisioni
Sezione intitolata “RevisionExtractor — aggiornamenti incrementali e revisioni”Un PDF firmato, annotato o comunque modificato dopo la creazione porta con sé aggiornamenti incrementali: ogni modifica accoda una nuova sezione di riferimenti incrociati e un trailer, terminando con un marcatore %%EOF. RevisionExtractor opera interamente tramite metodi statici su un PdfReader già analizzato:
extractRevision(string $pdfData, PdfReader $reader, int $revision)restituisce il file troncato al confine%%EOFdella revisione richiesta. La revisione0(la più recente) restituisce l’intero file; gli indici più alti restituiscono snapshot via via più vecchi.getRevisionBoundaries(string $pdfData, PdfReader $reader)restituisce unalist<array{revision, startByte, endByte, sizeBytes}>che descrive l’intervallo di byte apportato da ciascuna revisione.
Questo isolamento è deliberato: estraendo una revisione più vecchia si espongono solo gli oggetti visibili fino a quel punto, bloccando gli attacchi ibridi sui riferimenti incrociati in cui una revisione successiva ridefinisce un oggetto precedente.
Procedura guidata: ispezione di una revisione
Sezione intitolata “Procedura guidata: ispezione di una revisione”Questa procedura ispeziona la cronologia delle revisioni di un PDF che potrebbe essere stato modificato dopo la produzione da parte di Chrome. L’esempio è pronto per un contesto di produzione: dichiara tipi stretti, usa hint di tipo completi, convalida il proprio input e cattura l’eccezione più specifica.
- Leggere i byte del PDF in memoria e rifiutare l’input vuoto prima di costruire il reader.
- Costruire
\NextPDF\Parser\PdfReadere chiamareparse(). - Leggere
getRevisionCount(). Un valore di1indica un file a revisione singola senza aggiornamenti incrementali. - Per ciascuna revisione, leggere la relativa
RevisionXRefTablee ispezionaregetActiveObjectCount(),hasRootUpdate()egetSize(). - Calcolare gli intervalli di byte per revisione con
RevisionExtractor::getRevisionBoundaries(). - Catturare
PdfParseException— l’eccezione più specifica sollevata dal parser — e fornire un messaggio diagnostico.
<?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;}Il reader ordina le revisioni dalla più recente (index0) alla più vecchia. Per estrarre uno snapshot più vecchio come byte autonomi — ad esempio, per confrontare ciò che una modifica ha cambiato — chiamare direttamente l’estrattore:
<?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);}Gestione dei guasti
Sezione intitolata “Gestione dei guasti”Ogni guasto del parser emerge come NextPDF\Artisan\Exception\PdfParseException. Il messaggio localizza la causa. Usare la tabella seguente per mappare un frammento di messaggio alla fase che lo ha generato.
| Frammento di messaggio | Fase | Significato |
|---|---|---|
missing %PDF- header | PdfReader::parse() | I byte non sono un PDF oppure l’input è stato troncato in testa. |
Cannot find startxref marker / Invalid startxref offset | PdfReader::parse() | La coda del file è corrotta oppure il puntatore ai riferimenti incrociati è fuori dai limiti. |
Expected 'xref' keyword / Invalid xref subsection header | CrossRefParser::parseXRefTable() | Una tabella di riferimenti incrociati tradizionale è malformata. |
XRef stream ... /Type /XRef / invalid /W array | CrossRefParser::parseXRefStream() | A un flusso di riferimenti incrociati mancano voci di dizionario obbligatorie. |
exceeds limit of (conteggio di xref o di flusso di oggetti) | CrossRefParser / PdfReader | Un conteggio falsificato ha fatto scattare una protezione contro il denial-of-service. |
Unsupported stream filter | StreamDecoder::decode() | Il flusso usa un filtro al di fuori del set supportato FlateDecode / ASCIIHexDecode / ASCII85Decode. |
FlateDecode decompression failed / output exceeds ... bytes limit | StreamDecoder | I dati compressi non sono validi oppure si espandono oltre il tetto di 16 MiB. |
Maximum nesting depth ... exceeded / Keyword exceeds maximum length | PdfTokenizer | Una struttura artefatta o patologica ha fatto scattare un limite del tokenizer. |
Page index ... not found / out of range in subtree | PdfReader::getPage() | L’indice di pagina richiesto non esiste nell’albero delle pagine. |
Revision index ... out of range | PdfReader / RevisionExtractor | L’indice di revisione è al di fuori dell’intervallo da 0 a getRevisionCount() - 1. |
Quando si cattura l’eccezione, registrare il messaggio e il percorso di origine, quindi rilanciarla o restituire un errore definito. Non scartarla silenziosamente: un blocco catch vuoto nasconde l’unica informazione che il parser ha prodotto intenzionalmente.
<?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;}Impostazioni predefinite sicure
Sezione intitolata “Impostazioni predefinite sicure”- Chiamare sempre
parse()per primo. Ogni accessor diPdfReaderpresuppone che la catena dei riferimenti incrociati sia caricata. ChiamaregetObject()ogetPage()prima diparse()non restituisce nulla di utile. - Trattare il parser come di sola lettura e modellato su Chrome. Punta al sottoinsieme della sintassi PDF emesso da
printToPDFdi Chrome. I PDF cifrati, le tabelle di hint linearizzate e gli aggiornamenti incrementali in conflitto sono fuori ambito per progettazione. Non estenderlo a strumento generico di riparazione PDF. - Mantenere i limiti di sicurezza al loro posto. I limiti su annidamento, lunghezza delle parole chiave, dimensione degli array, conteggio dei riferimenti incrociati e decompressione esistono per contenere l’uso di risorse su input ostile. Una
PdfParseExceptiongenerata da un limite è l’esito corretto per un file artefatto; alzare un limite per accettare un file del genere amplia la superficie di attacco. - Per impostazione predefinita, usare la pagina
0.getPage()ePageImporter::import()usano per impostazione predefinita la prima pagina. Scegliere un altro indice solo quando il flusso di lavoro lo richiede deliberatamente. - Convalidare l’input prima di costruire il reader. Rifiutare subito i byte vuoti o illeggibili, come fanno gli esempi precedenti, così che un chiaro errore a livello di applicazione preceda qualsiasi eccezione del parser.
- Catturare
PdfParseException, mai un\Exceptionnudo. È l’unico tipo specifico sollevato dal parser; catturarlo evita che guasti non correlati vengano mascherati.
Vedere anche
Sezione intitolata “Vedere anche”- Guida per sviluppatori di Artisan — il confine di importazione al di sopra del parser, inclusi
ChromeHtmlRenderer,PageImportere la stratificazione architetturale. - Riferimento API di Artisan — le tabelle dei metodi pubblicate per la superficie pubblica del pacchetto.
- Risoluzione dei problemi di Artisan — guida orientata ai sintomi per i guasti del renderer e dell’importazione.
- Configurazione del renderer Chrome — configurazione del renderer che produce i PDF che questo parser legge.
- ISO 32000-2:2020 §7.5 (struttura del file, riferimenti incrociati, aggiornamenti incrementali) e §7.2 (convenzioni lessicali) — la specifica implementata dal tokenizer e dal parser dei riferimenti incrociati. Consultare lo standard pubblicato per il formato autorevole a livello di byte.