İçeriğe geç

Gelişmiş PDF ayrıştırıcısı tanılaması

Artisan 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.

Ayrış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() bir NextPDF\Artisan\Exception\PdfParseException fı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ı, 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ıfSorumlulukTemel yöntemler
PdfReaderDosya 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()
PdfTokenizerISO 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()
CrossRefParserGeleneksel çapraz başvuru tablolarını ve çapraz başvuru akışlarını ayrıştırır.parseXRefTable(), parseXRefStream()
StreamDecoderAkış baytlarının kodunu /Filter değerine göre çözer.decode() (statik)
ResourceCollectorBir Resources ağacını özyinelemeli olarak dolaşır ve erişilebilir her dolaylı nesneyi toplar.traverse(), getCollected()
RevisionExtractorArtımlı olarak güncellenmiş bir dosyayı düzeltme başına bayt aralıklarına böler.extractRevision() (statik), getRevisionBoundaries() (statik)
PdfObjectDeğ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()
RevisionXRefTableDüzeltme başına değişmez çapraz başvuru anlık görüntüsü.getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize()

Ham 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) bir PdfObject dö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ı bir list<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, /Pages ağacında dolaşır ve sıfır tabanlı dizindeki sayfayı döndürür. getPageContentStream(), getPageResources() ve getPageMediaBox(), PageImporter tarafından ihtiyaç duyulan bölümleri çıkarır. collectPageResources(), sayfanın Resources ve Contents bölümlerinden erişilebilen her nesne için bir array<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 dosya 1 döndürür. getRevisionXRef(int $index) tek bir RevisionXRefTable döndürür (0 dizini en güncel olanıdır). getRevisions() tam list<RevisionXRefTable> değerini döndürür.

PdfTokenizer 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 PdfParseException fı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şlemi readName(), readLiteralString(), readHexString(), readArray(), readDictionary() veya bir number/reference okuyucusuna devreder. Dolaylı bir başvuru olan N G R, ['type' => 'ref', 'num' => N, 'gen' => G] dizisi olarak döndürülür. PdfObject::getRef() ve PdfReader::resolveRef() bu biçimi tanır.

CrossRefParser, Chrome’un üretebildiği iki biçimi de ayrıştırır:

  • parseXRefTable() geleneksel bir xref tablosunu okur (PDF 1.x biçimi): alt bölüm başlıkları, sabit genişlikli 20 baytlık girdiler ve ardından bir trailer sözlüğü.
  • parseXRefStream() bir çapraz başvuru akışını okur (PDF 2.0, ISO 32000-2:2020 §7.5.8): /Type /XRef içeren bir dolaylı nesne, alan genişliği dizisi olan bir /W ve 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::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)
  • ASCIIHexDecode
  • ASCII85Decode

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, 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üm

Oluş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 %%EOF sınırında kesilmiş olarak döndürür. 0 dü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 bir list<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.

Bu 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.

  1. PDF baytlarını belleğe okuyun ve okuyucuyu oluşturmadan önce boş girdiyi reddedin.
  2. Bir \NextPDF\Parser\PdfReader nesnesi oluşturun ve parse() çağrısını yapın.
  3. Ardından getRevisionCount() değerini okuyun. 1 değeri, artımlı güncellemesi olmayan tek düzeltmeden oluşan bir dosya anlamına gelir.
  4. Her düzeltme için, ilgili RevisionXRefTable değerini okuyun ve getActiveObjectCount(), hasRootUpdate() ve getSize() değerlerini inceleyin.
  5. Düzeltme başına bayt aralıklarını RevisionExtractor::getRevisionBoundaries() ile hesaplayın.
  6. 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.
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;
}

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:

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);
}

Her 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şamaAnlamı
missing %PDF- headerPdfReader::parse()Baytlar bir PDF değildir veya girdi başından kesilmiştir.
Cannot find startxref marker / Invalid startxref offsetPdfReader::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 headerCrossRefParser::parseXRefTable()Geleneksel bir çapraz başvuru tablosu hatalı biçimlendirilmiştir.
XRef stream ... /Type /XRef / invalid /W arrayCrossRefParser::parseXRefStream()Bir çapraz başvuru akışında gerekli sözlük girdileri eksiktir.
exceeds limit of (xref veya nesne akışı sayısı)CrossRefParser / PdfReaderSahte bir sayı, bir hizmet reddi korumasını tetikledi.
Unsupported stream filterStreamDecoder::decode()Akış, desteklenen FlateDecode / ASCIIHexDecode / ASCII85Decode kümesinin dışında bir süzgeç kullanır.
FlateDecode decompression failed / output exceeds ... bytes limitStreamDecoderSı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 lengthPdfTokenizerÖzel olarak hazırlanmış veya patolojik bir yapı, bir belirteçleyici sınırını tetikledi.
Page index ... not found / out of range in subtreePdfReader::getPage()İstenen sayfa dizini, sayfa ağacında bulunmaz.
Revision index ... out of rangePdfReader / RevisionExtractorDü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.

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;
}
  • 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() veya getPage() çağrısının parse() ç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 printToPDF tarafı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 0 sayfasını kullanın. getPage() ve PageImporter::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 \Exception değ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.
  • Artisan geliştirici kılavuzu — ayrıştırıcının üstündeki içe aktarma sınırı; ChromeHtmlRenderer, PageImporter ve 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.