Gelişmiş PDF ayrıştırıcısı tanılaması
Genel bakış
“Genel bakış” başlıklı bölümArtisan içe aktarma yolu, Chrome’un oluşturduğu bir Taşınabilir Belge Biçimi (PDF) dosyasını okur ve bir sayfayı NextPDF belgesine aktarır. Zorlayıcı bir girdi bu içe aktarmanın başarısız olmasına neden olduğunda, PageImporter::import() altındaki, dosyayı bayt bayt okuyan ayrıştırıcı sınıflarına bakın.
Bu kılavuz, NextPDF\Parser ad alanındaki düşük seviyeli ayrıştırıcı yüzeyini kapsar: PdfReader, PdfTokenizer, CrossRefParser, StreamDecoder, ResourceCollector, RevisionExtractor ve PdfObject ile RevisionXRefTable değer nesneleri. Burada gösterilen her sembol nextpdf/artisan içinde bulunur. Kılavuz, ayrıştırıcıyı idealize edilmiş bir arabirim olarak değil, gerçekte kurulduğu biçimiyle açıklar.
Bu kılavuzu hem açıklayıcı kaynak hem de uygulama rehberi olarak kullanın. Bileşenlerin nasıl bir araya geldiğini gösterir, ardından bir artımlı güncelleme düzeltmesini inceleme adımlarında size yol gösterir. Bu katmanın üstündeki içe aktarma sınırı için Artisan geliştirici kılavuzuna bakın.
Buna ne zaman ihtiyaç duyarsınız
“Buna ne zaman ihtiyaç duyarsınız” başlıklı bölümAyrıştırıcı yüzeyini yalnızca normal içe aktarma yolu zaten başarısız olduğunda ve nedenini bulmanız gerektiğinde kullanın. Yaygın tetikleyiciler şunlardır:
PageImporter::import()birNextPDF\Artisan\Exception\PdfParseExceptionfırlatır ve hatanın çapraz başvuru tablosunda mı, bir akış süzgecinde mi yoksa sayfa ağacında mı olduğunu bilmeniz gerekir.- Bir Chrome yükseltmesi çıktı biçimini değiştirir; örneğin geleneksel bir çapraz başvuru tablosu çapraz başvuru akışına dönüştüğünde ya da tersi olduğunda, test düzenekleriniz artık eşleşmez.
- Chrome’un oluşturmadığı, üçüncü taraf bir PDF alırsınız ve ayrıştırıcının onu okuyup okuyamayacağını doğrulamak istersiniz.
- Artımlı olarak güncellenmiş bir belgeyi inceliyorsunuz ve her düzeltmeye ait bayt aralıklarına veya nesne görünürlüğüne ihtiyacınız var.
Normal bir işleyici tümleştirmesi yazıyorsanız, bu yüzeye ihtiyacınız yoktur. Ayrıştırıcı, genel amaçlı bir PDF kitaplığı değil, dahili bir tanılama aracıdır. Şifrelenmiş PDF’leri, doğrusallaştırılmış ipucu tablolarını veya çelişen nesne yeniden tanımlamaları içeren artımlı güncellemeleri desteklemez.
Ayrıştırıcı yüzeyi
“Ayrıştırıcı yüzeyi” başlıklı bölümAyrıştırıcı, her biri tek sorumluluk taşıyan küçük sınıflardan oluşur. PdfReader giriş noktasıdır. Diğer sınıflar, onun oluşturduğu veya çağırdığı iş birlikçi sınıflardır.
| Sınıf | Sorumluluk | Temel yöntemler |
|---|---|---|
PdfReader | Dosya yapısını okur, nesneleri çözer ve sayfa ağacını dolaşır. | parse(), getObject(), getTrailer(), getObjectNumbers(), getPage(), getPageContentStream(), getPageResources(), getPageMediaBox(), resolveRef(), collectPageResources(), getRevisionCount(), getRevisionXRef(), getRevisions() |
PdfTokenizer | ISO 32000-2:2020 §7.2 uyarınca sözcüksel söz dizimini ayrıştırır: adlar, dizeler, sayılar, sözlükler, diziler ve başvurular. | readToken(), readValue(), readName(), readNumber(), readDictionary(), readArray(), readStreamData(), peek(), skipWhitespace(), getOffset(), setOffset() |
CrossRefParser | Geleneksel çapraz başvuru tablolarını ve çapraz başvuru akışlarını ayrıştırır. | parseXRefTable(), parseXRefStream() |
StreamDecoder | Akış baytlarının kodunu /Filter değerine göre çözer. | decode() (statik) |
ResourceCollector | Bir Resources ağacını özyinelemeli olarak dolaşır ve erişilebilir her dolaylı nesneyi toplar. | traverse(), getCollected() |
RevisionExtractor | Artımlı olarak güncellenmiş bir dosyayı düzeltme başına bayt aralıklarına böler. | extractRevision() (statik), getRevisionBoundaries() (statik) |
PdfObject | Değişmez, ayrıştırılmış dolaylı nesne (sözlük ve isteğe bağlı akış). | get(), getRef(), getArray(), getType(), getSubtype(), hasStream(), getDictionary(), getRawStreamData(), getRawDictionaryBytes() |
RevisionXRefTable | Düzeltme başına değişmez çapraz başvuru anlık görüntüsü. | getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize() |
PdfReader — giriş noktası
“PdfReader — giriş noktası” başlıklı bölümHam PDF baytlarıyla \NextPDF\Parser\PdfReader nesnesini oluşturun, ardından başka bir yöntem çağırmadan önce parse() çağrısını yapın. parse() yöntemi %PDF- başlığını denetler, dosyanın sonunda startxref değerini bulur ve /Prev bağlantılarını izleyerek çapraz başvuru zincirini dolaşır.
Okuyucu, parse() çağrısından sonra üç yöntem grubuna erişim sağlar:
- Nesne erişimi.
getObject(int $objNum)birPdfObjectdöndürür ve Tür 2 girdilerini (bir nesne akışı içinde saklanan nesneleri) otomatik olarak çözer.getObjectNumbers(), boş olmayan tüm nesne numaralarının sıralı birlist<int>değerini döndürür.resolveRef(mixed $value)tek bir dolaylı başvuruyu izler. Doğrudan değerler değişmeden geçer. - Sayfa erişimi.
getPage(int $pageIndex)kataloğu çözer,/Pagesağacında dolaşır ve sıfır tabanlı dizindeki sayfayı döndürür.getPageContentStream(),getPageResources()vegetPageMediaBox(),PageImportertarafından ihtiyaç duyulan bölümleri çıkarır.collectPageResources(), sayfanın Resources ve Contents bölümlerinden erişilebilen her nesne için birarray<int, PdfObject>döndürür. - Düzeltme erişimi.
getRevisionCount()artımlı düzeltmelerin sayısını döndürür. Tek düzeltmeden oluşan bir dosya1döndürür.getRevisionXRef(int $index)tek birRevisionXRefTabledöndürür (0dizini en güncel olanıdır).getRevisions()tamlist<RevisionXRefTable>değerini döndürür.
PdfTokenizer — sözcüksel çözümleme
“PdfTokenizer — sözcüksel çözümleme” başlıklı bölümPdfTokenizer bayt akışını okur. Bu sınıfı nadiren kendiniz oluşturursunuz, çünkü örnekleri PdfReader ve CrossRefParser tarafından yönetilir. Ayrıştırma hatalı biçimlendirilmiş bir belirteç nedeniyle başarısız olduğunda bu katmanı inceleyin. Tanılama açısından iki davranış önemlidir:
- Güvenlik sınırları yapılandırılabilir değildir; sabittir. Belirteçleyici; değişmez dize iç içe geçmesini, sözlük ve dizi iç içe geçmesini, anahtar sözcük uzunluğunu ve dizi öğesi sayısını sınırlandırır. Girdi bir sınırı aştığında, bir
PdfParseExceptionfırlatır ve iletide sınırı adlandırır. Özel olarak hazırlanmış bir girdinin bu sınırlardan birini tetiklemesi, bir ayrıştırıcı hatası değil; tasarım gereği çalışan bir savunmadır. readValue()ayrıştırmayı yönlendirir. Bir sonraki baytı inceler ve işlemireadName(),readLiteralString(),readHexString(),readArray(),readDictionary()veya bir number/reference okuyucusuna devreder. Dolaylı bir başvuru olanN G R,['type' => 'ref', 'num' => N, 'gen' => G]dizisi olarak döndürülür.PdfObject::getRef()vePdfReader::resolveRef()bu biçimi tanır.
CrossRefParser — çapraz başvuru çözümleme
“CrossRefParser — çapraz başvuru çözümleme” başlıklı bölümCrossRefParser, Chrome’un üretebildiği iki biçimi de ayrıştırır:
parseXRefTable()geleneksel birxreftablosunu okur (PDF 1.x biçimi): alt bölüm başlıkları, sabit genişlikli 20 baytlık girdiler ve ardından birtrailersözlüğü.parseXRefStream()bir çapraz başvuru akışını okur (PDF 2.0, ISO 32000-2:2020 §7.5.8):/Type /XRefiçeren bir dolaylı nesne, alan genişliği dizisi olan bir/Wve girdilerden oluşan bir ikili akış.
Her ikisi de aynı biçimi döndürür: array{xref: array<int, ...>, trailer: array<string, mixed>, prevOffset: int|null}. PdfReader::parse(), çapraz başvuru uzaklığındaki dört bayta göz atarak hangi ayrıştırıcının çağrılacağına karar verir: xref ise tablo ayrıştırıcısını seçer; bunun dışındaki her şey bir akış nesnesi olarak ele alınır. Her iki ayrıştırıcı da, aksi takdirde ayrıştırıcının gereğinden fazla çalışmasına yol açacak sahte sayıları reddetmek için alt bölüm başına bir milyon girdilik bir üst sınır uygular.
StreamDecoder — akış süzgeçleri
“StreamDecoder — akış süzgeçleri” başlıklı bölümStreamDecoder::decode(string $data, string|array $filter) statiktir ve tek bir süzgeci veya zincirlenmiş bir süzgeç listesini uygular. Yalnızca Chrome’un printToPDF tarafından üretilen süzgeçleri destekler:
FlateDecode(zlib, ham deflate yedeğiyle birlikte)ASCIIHexDecodeASCII85Decode
Diğer herhangi bir süzgeç adı, PdfParseException fırlatır; ileti Unsupported stream filter olur. Kod çözücü, sıkıştırma bombası riskini sınırlandırmak için açılmış çıktıyı 16 MiB ile sınırlar. Aşırı büyük bir çıktı, sınırsız bellek ayırmak yerine hata fırlatır. PdfReader bir akış okuduğunda ve kod çözme sırasında hata fırlatıldığında, ham akış baytlarına geri döner; böylece tek bir bozuk süzgeç tüm ayrıştırmayı durdurmaz.
ResourceCollector — derin kaynak dolaşımı
“ResourceCollector — derin kaynak dolaşımı” başlıklı bölümResourceCollector, bir PdfReader ile oluşturulur ve PdfReader::collectPageResources() aracılığıyla çağrılır. traverse() yöntemi bir değeri özyinelemeli olarak dolaşır, her ['type' => 'ref'] başvurusunu getObject() aracılığıyla izler ve çözülen her nesneyi bir kez, nesne numarasına göre anahtarlanan bir array<int, PdfObject> içine kaydeder. Özyineleme derinliğini sınırlar ve çözümleyemediği başvuruları sessizce atlar; böylece sarkan tek bir başvuru, katı bir hata yerine kısmi bir koleksiyon üretir.
RevisionExtractor — artımlı güncellemeler ve düzeltmeler
“RevisionExtractor — artımlı güncellemeler ve düzeltmeler” başlıklı bölümOluşturulduktan sonra imzalanmış, açıklama eklenmiş veya başka bir biçimde düzenlenmiş bir PDF, artımlı güncellemeler taşır. Her düzenleme, bir %%EOF işaretiyle biten yeni bir çapraz başvuru bölümü ve sonbilgisi ekler. RevisionExtractor, ayrıştırılmış bir PdfReader üzerinde tamamen statik yöntemlerle çalışır:
extractRevision(string $pdfData, PdfReader $reader, int $revision), dosyayı istenen düzeltmenin%%EOFsınırında kesilmiş olarak döndürür.0düzeltmesi (en güncel olanı) dosyanın tamamını döndürür; daha yüksek dizinler giderek daha eski anlık görüntüleri döndürür.getRevisionBoundaries(string $pdfData, PdfReader $reader), her düzeltmenin katkıda bulunduğu bayt aralığını açıklayan birlist<array{revision, startByte, endByte, sizeBytes}>döndürür.
Bu yalıtım kasıtlıdır. Daha eski bir düzeltmeyi çıkarmak, yalnızca o noktaya kadar görünür olan nesneleri açığa çıkarır; bu da daha sonraki bir düzeltmenin önceki bir nesneyi yeniden tanımladığı melez çapraz başvuru saldırılarını engeller.
Adım adım: bir düzeltmeyi inceleme
“Adım adım: bir düzeltmeyi inceleme” başlıklı bölümBu yordam, Chrome tarafından oluşturulduktan sonra düzenlenmiş olabilecek bir PDF’nin düzeltme geçmişini inceler. Örnek, üretime uygun biçimdedir: katı türleri bildirir, tam tür ipuçları kullanır, girdisini doğrular ve en belirli özel durumu yakalar.
- PDF baytlarını belleğe okuyun ve okuyucuyu oluşturmadan önce boş girdiyi reddedin.
- Bir
\NextPDF\Parser\PdfReadernesnesi oluşturun veparse()çağrısını yapın. - Ardından
getRevisionCount()değerini okuyun.1değeri, artımlı güncellemesi olmayan tek düzeltmeden oluşan bir dosya anlamına gelir. - Her düzeltme için, ilgili
RevisionXRefTabledeğerini okuyun vegetActiveObjectCount(),hasRootUpdate()vegetSize()değerlerini inceleyin. - Düzeltme başına bayt aralıklarını
RevisionExtractor::getRevisionBoundaries()ile hesaplayın. - Ayrıştırıcının fırlattığı en belirli özel durum olan
PdfParseExceptionözel durumunu yakalayın ve bir tanılama iletisi gösterin.
<?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;}Okuyucu, düzeltmeleri en yenisinden (index0) en eskisine doğru sıralar. Daha eski bir anlık görüntüyü bağımsız baytlar olarak çıkarmak, örneğin bir düzenlemenin neyi değiştirdiğini karşılaştırmak için çıkarıcıyı doğrudan çağırın:
<?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);}Hata işleme
“Hata işleme” başlıklı bölümHer ayrıştırıcı hatası bir NextPDF\Artisan\Exception\PdfParseException olarak ortaya çıkar. İleti nedeni açıklar. Bir ileti parçasını, onu fırlatan aşamaya eşlemek için aşağıdaki tabloyu kullanın.
| İleti parçası | Aşama | Anlamı |
|---|---|---|
missing %PDF- header | PdfReader::parse() | Baytlar bir PDF değildir veya girdi başından kesilmiştir. |
Cannot find startxref marker / Invalid startxref offset | PdfReader::parse() | Dosyanın sonu bozuk veya çapraz başvuru işaretçisi sınırların dışındadır. |
Expected 'xref' keyword / Invalid xref subsection header | CrossRefParser::parseXRefTable() | Geleneksel bir çapraz başvuru tablosu hatalı biçimlendirilmiştir. |
XRef stream ... /Type /XRef / invalid /W array | CrossRefParser::parseXRefStream() | Bir çapraz başvuru akışında gerekli sözlük girdileri eksiktir. |
exceeds limit of (xref veya nesne akışı sayısı) | CrossRefParser / PdfReader | Sahte bir sayı, bir hizmet reddi korumasını tetikledi. |
Unsupported stream filter | StreamDecoder::decode() | Akış, desteklenen FlateDecode / ASCIIHexDecode / ASCII85Decode kümesinin dışında bir süzgeç kullanır. |
FlateDecode decompression failed / output exceeds ... bytes limit | StreamDecoder | Sıkıştırılmış veri geçersizdir veya 16 MiB üst sınırını aşacak şekilde genişler. |
Maximum nesting depth ... exceeded / Keyword exceeds maximum length | PdfTokenizer | Özel olarak hazırlanmış veya patolojik bir yapı, bir belirteçleyici sınırını tetikledi. |
Page index ... not found / out of range in subtree | PdfReader::getPage() | İstenen sayfa dizini, sayfa ağacında bulunmaz. |
Revision index ... out of range | PdfReader / RevisionExtractor | Düzeltme dizini, 0 ile getRevisionCount() - 1 aralığının dışındadır. |
Özel durumu yakaladığınızda, iletiyi ve kaynak yolunu günlüğe kaydedin, ardından özel durumu yeniden fırlatın veya tanımlı bir hata döndürün. Bunu sessizce yok saymayın. Boş bir catch bloğu, ayrıştırıcının üretmeye çalıştığı tek bilgi parçasını gizler.
<?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;}Güvenli varsayılanlar
“Güvenli varsayılanlar” başlıklı bölüm- Her zaman önce
parse()çağrısını yapın.PdfReaderüzerindeki her erişimci, çapraz başvuru zincirinin yüklenmiş olduğunu varsayar.getObject()veyagetPage()çağrısınınparse()çağrılmadan önce yapılması yararlı bir sonuç üretmez. - Ayrıştırıcıyı salt okunur ve Chrome biçimine yönelik olarak ele alın. Chrome’un
printToPDFtarafından üretilen PDF söz dizimi alt kümesini hedefler. Şifrelenmiş PDF’ler, doğrusallaştırılmış ipucu tabloları ve çelişen artımlı güncellemeler tasarım gereği kapsam dışıdır. Onu genel bir PDF onarım aracına dönüştürmeyin. - Güvenlik sınırlarını yerinde tutun. İç içe geçme, anahtar sözcük uzunluğu, dizi boyutu, çapraz başvuru sayısı ve açma üst sınırları, düşmanca girdilerde kaynak kullanımını sınırlandırır. Bir sınırdan kaynaklanan bir
PdfParseException, özel olarak hazırlanmış bir dosya için doğru sonuçtur. Böyle bir dosyayı kabul etmek için bir sınırı yükseltmek saldırı yüzeyini genişletir. - Varsayılan olarak
0sayfasını kullanın.getPage()vePageImporter::import()varsayılan olarak ilk sayfayı kullanır. Başka bir dizini yalnızca iş akışı bunu bilinçli olarak gerektirdiğinde seçin. - Okuyucuyu oluşturmadan önce girdiyi doğrulayın. Yukarıdaki örneklerde olduğu gibi, boş veya okunamayan baytları erken reddedin; böylece herhangi bir ayrıştırıcı özel durumundan önce açık bir uygulama düzeyi hatası görünür.
- Asla yalın
\Exceptiondeğil,PdfParseExceptionözel durumunu yakalayın. Ayrıştırıcının fırlattığı tek belirli türdür. Onu yakalamak, ilgisiz hataların maskelenmesini önler.
Ayrıca bkz.
“Ayrıca bkz.” başlıklı bölüm- Artisan geliştirici kılavuzu — ayrıştırıcının üstündeki içe aktarma sınırı;
ChromeHtmlRenderer,PageImporterve mimari katmanlar dahil. - Artisan API başvurusu — paketin genel yüzeyi için yayımlanmış yöntem tabloları.
- Artisan sorun giderme — işleyici ve içe aktarma hataları için belirti odaklı rehberlik.
- Chrome işleyici kurulumu — bu ayrıştırıcının okuduğu PDF’leri üreten işleyiciyi yapılandırma.
- ISO 32000-2:2020 §7.5 (dosya yapısı, çapraz başvuru, artımlı güncellemeler) ve §7.2 (sözcüksel kurallar) — belirteçleyicinin ve çapraz başvuru ayrıştırıcısının uyguladığı belirtim. Yetkili bayt düzeyi biçim için yayımlanmış standarda başvurun.