تشخيص محلّل PDF المتقدّم
لمحة سريعة
قسم بعنوان «لمحة سريعة»يقرأ مسار الاستيراد في Artisan ملف Portable Document Format (PDF) الذي يُنشئه Chrome ويجلب صفحة واحدة إلى مستند NextPDF. عندما يمنع مُدخَل معقّد إكمال هذا الاستيراد، انظر أسفل PageImporter::import() إلى أصناف المحلّل التي تقرأ الملف بايتًا بايتًا.
يغطّي هذا الدليل سطح المحلّل منخفض المستوى في فضاء الأسماء NextPDF\Parser: PdfReader، وPdfTokenizer، وCrossRefParser، وStreamDecoder، وResourceCollector، وRevisionExtractor، وكائنا القيمة PdfObject وRevisionXRefTable. كل رمز معروض هنا موجود في nextpdf/artisan. يصف الدليل المحلّل كما هو منفّذ فعليًّا، لا بوصفه واجهة مثالية.
استعمل هذا الدليل بوصفه شرحًا ومرجعًا عمليًّا في الوقت نفسه. فهو يوضّح كيف تتكامل الأجزاء، ثم يرشدك خطوةً بخطوة إلى فحص مراجعة تحديث تزايدي. لمعرفة حدود الاستيراد فوق هذه الطبقة، راجع دليل مطوّري Artisan.
متى تحتاج إلى هذا
قسم بعنوان «متى تحتاج إلى هذا»استعمل سطح المحلّل فقط عندما يكون مسار الاستيراد الاعتيادي قد فشل بالفعل وتحتاج إلى تحديد السبب. تشمل الحالات النموذجية ما يلي:
- يطرح
PageImporter::import()استثناءNextPDF\Artisan\Exception\PdfParseException، وتحتاج إلى معرفة ما إذا كان الخلل في جدول المراجع المتقاطعة، أو في مرشّح دفق، أو في شجرة الصفحات. - تغيّر ترقية في Chrome صيغة المُخرَج، كأن يتحوّل جدول مراجع متقاطعة تقليدي إلى دفق مراجع متقاطعة أو العكس، فتتوقّف أدواتك الثابتة عن المطابقة.
- تستلم ملف PDF من طرف ثالث لم يُنتِجه Chrome، وتريد التأكّد مما إذا كان المحلّل يستطيع قراءته أصلًا.
- تحلّل مستندًا محدَّثًا تزايديًّا وتحتاج إلى نطاقات بايت لكل مراجعة أو إلى رؤية الكائنات.
إن كنت تكتب تكامل عرض اعتياديًّا، فلن تحتاج إلى هذا السطح. المحلّل أداة تشخيص داخلية، وليس مكتبة PDF عامة الغرض. لا يدعم ملفات PDF المشفّرة، ولا جداول التلميح الخطّية، ولا التحديثات التزايدية التي تتضمّن إعادات تعريف متعارضة للكائنات.
سطح المحلِّل
قسم بعنوان «سطح المحلِّل»المحلّل مجموعة صغيرة من الأصناف أحادية المسؤولية. PdfReader هو نقطة الدخول، أما الأصناف الأخرى فهي عناصر مساعدة يُنشئها أو يستدعيها.
| الصنف | المسؤولية | الطرائق الرئيسة |
|---|---|---|
PdfReader | قراءة بنية الملف، وحلّ الكائنات، واجتياز شجرة الصفحات. | parse(), getObject(), getTrailer(), getObjectNumbers(), getPage(), getPageContentStream(), getPageResources(), getPageMediaBox(), resolveRef(), collectPageResources(), getRevisionCount(), getRevisionXRef(), getRevisions() |
PdfTokenizer | تحليل البنية المعجمية وفق ISO 32000-2:2020 §7.2: الأسماء، والسلاسل، والأرقام، والقواميس، والمصفوفات، والمراجع. | readToken(), readValue(), readName(), readNumber(), readDictionary(), readArray(), readStreamData(), peek(), skipWhitespace(), getOffset(), setOffset() |
CrossRefParser | تحليل جداول المراجع المتقاطعة التقليدية ودفوق المراجع المتقاطعة. | parseXRefTable(), parseXRefStream() |
StreamDecoder | فكّ ترميز بايتات الدفق بحسب /Filter. | decode() (ساكنة) |
ResourceCollector | اجتياز شجرة الموارد تعاوديًّا وجمع كل كائن غير مباشر يمكن الوصول إليه. | traverse(), getCollected() |
RevisionExtractor | تقطيع ملف محدَّث تزايديًّا إلى نطاقات بايت لكل مراجعة. | extractRevision() (ساكنة)، getRevisionBoundaries() (ساكنة) |
PdfObject | كائن غير مباشر مُحلَّل وثابت (قاموس بالإضافة إلى دفق اختياري). | get(), getRef(), getArray(), getType(), getSubtype(), hasStream(), getDictionary(), getRawStreamData(), getRawDictionaryBytes() |
RevisionXRefTable | لقطة مراجع متقاطعة ثابتة لكل مراجعة. | getObjectNumbers(), getActiveObjectCount(), hasRootUpdate(), getSize() |
PdfReader — نقطة الدخول
قسم بعنوان «PdfReader — نقطة الدخول»أنشئ \NextPDF\Parser\PdfReader باستخدام بايتات PDF الخام، ثم استدعِ parse() قبل استدعاء أي طريقة أخرى. تتحقّق parse() من ترويسة %PDF-، وتعثر على startxref في ذيل الملف، وتجتاز سلسلة المراجع المتقاطعة باتّباع روابط /Prev.
بعد parse()، يوفّر القارئ ثلاث مجموعات من الطرائق:
- الوصول إلى الكائنات. يُعيد
getObject(int $objNum)كائنPdfObject، ويحلّ مدخلات النوع 2 (الكائنات المخزَّنة داخل دفق كائنات) تلقائيًّا. يُعيدgetObjectNumbers()قيمةlist<int>مرتَّبة لكل رقم كائن غير حرّ. يتّبعresolveRef(mixed $value)مرجعًا غير مباشر واحدًا. أما القيمة المباشرة فتمرّ من دون تغيير. - الوصول إلى الصفحات. يحلّ
getPage(int $pageIndex)الفهرس، ويجتاز/Pages، ويُعيد الصفحة عند الفهرس المبدوء من صفر. تستخرجgetPageContentStream()، وgetPageResources()، وgetPageMediaBox()الأجزاء التي يحتاجهاPageImporter. يُعيدcollectPageResources()قيمةarray<int, PdfObject>لكل كائن يمكن الوصول إليه من موارد الصفحة ومحتوياتها. - الوصول إلى المراجعات. يُعيد
getRevisionCount()عدد المراجعات التزايدية. الملف ذو المراجعة الواحدة يُعيد1. يُعيدgetRevisionXRef(int $index)كائنRevisionXRefTableواحدًا (الفهرس0هو الأحدث). ويُعيدgetRevisions()قيمةlist<RevisionXRefTable>الكاملة.
PdfTokenizer — التحليل المعجمي
قسم بعنوان «PdfTokenizer — التحليل المعجمي»يقرأ PdfTokenizer دفق البايتات. نادرًا ما تنشئه بنفسك لأن PdfReader وCrossRefParser يستخدمان نسخهما الخاصة. افحص هذه الطبقة عندما يفشل التحليل عند رمز مُشوَّه. يهمّك سلوكان عند التشخيص:
- حدود الأمان ثوابت، لا إعدادات. يحدّ المحلّل من تداخل السلاسل النصّية الحرفية، وتداخل القواميس والمصفوفات، وطول الكلمات المفتاحية، وعدد عناصر المصفوفة. عندما يتجاوز المُدخَل حدًّا، يطرح
PdfParseExceptionويذكر ذلك الحدّ في الرسالة. المُدخَل المُصاغ خصّيصًا لتجاوز أحد هذه الحدود يدل على دفاع يعمل كما صُمِّم، لا على عيب في المحلِّل. readValue()يوجّه التحليل. يفحص البايت التالي ويفوّض إلىreadName()، وreadLiteralString()، وreadHexString()، وreadArray()، وreadDictionary()، أو قارئ number/reference. يُعاد المرجع غير المباشرN G Rعلى هيئة المصفوفة['type' => 'ref', 'num' => N, 'gen' => G]. يتعرّفPdfObject::getRef()وPdfReader::resolveRef()على هذه الهيئة.
CrossRefParser — حلّ المراجع المتقاطعة
قسم بعنوان «CrossRefParser — حلّ المراجع المتقاطعة»يحلّل CrossRefParser الصيغتين اللتين قد يصدرهما Chrome:
parseXRefTable()يقرأ جدولxrefتقليديًّا (بأسلوب PDF 1.x): ترويسات الأقسام الفرعية، ومدخلات ثابتة العرض بحجم 20 بايتًا، ثم قاموسtrailer.parseXRefStream()يقرأ دفق مراجع متقاطعة (PDF 2.0، ISO 32000-2:2020 §7.5.8): كائن غير مباشر يحمل/Type /XRef، ومصفوفة عرض حقول/W، ودفقًا ثنائيًّا من المدخلات.
كلاهما يُعيد الهيئة نفسها: array{xref: array<int, ...>, trailer: array<string, mixed>, prevOffset: int|null}. يقرّر PdfReader::parse() أي محلِّل يستدعي عبر استراق النظر إلى البايتات الأربعة عند إزاحة المراجع المتقاطعة: xref يختار محلِّل الجدول، وأي شيء آخر يُعامَل بوصفه كائن دفق. يفرض كلا المحلِّلين سقفًا قدره مليون مدخل لكل قسم فرعي لرفض الأعداد المزوَّرة التي قد تجعل المحلِّل يعمل بإفراط.
StreamDecoder — مرشّحات الدفق
قسم بعنوان «StreamDecoder — مرشّحات الدفق»StreamDecoder::decode(string $data, string|array $filter) طريقة ساكنة تطبّق مرشّحًا واحدًا أو قائمة مرشّحات متسلسلة. وهي لا تدعم إلا المرشّحات التي يصدرها printToPDF في Chrome:
FlateDecode(zlib، مع احتياطي raw-deflate)ASCIIHexDecodeASCII85Decode
أي اسم مرشّح آخر يطرح PdfParseException مع Unsupported stream filter. يحدّ المُفكِّك المُخرَج المفكوك ضغطه عند 16 MiB لتقليل خطر قنبلة فكّ الضغط. المُخرَج المفرط الحجم يطرح استثناءً بدلًا من التخصيص بلا حدّ. عندما يقرأ PdfReader دفقًا ويطرح فكّ الترميز خطأً، يلجأ إلى بايتات الدفق الخام، فلا يُفشِل مرشّح واحد تالف التحليل بأكمله.
ResourceCollector — الاجتياز العميق للموارد
قسم بعنوان «ResourceCollector — الاجتياز العميق للموارد»يُنشأ ResourceCollector باستخدام PdfReader ويُستدعى عبر PdfReader::collectPageResources(). طريقته traverse() تجتاز قيمة تعاوديًّا، وتتّبع كل مرجع ['type' => 'ref'] عبر getObject()، وتسجّل كل كائن محلول مرة واحدة في array<int, PdfObject> مفهرسة برقم الكائن. تحدّ من عمق التعاود وتتخطّى بصمت المراجع التي لا تستطيع حلّها، فيُنتِج مرجع معلّق واحد مجموعة جزئية بدلًا من فشل قاطع.
RevisionExtractor — التحديثات التزايدية والمراجعات
قسم بعنوان «RevisionExtractor — التحديثات التزايدية والمراجعات»ملف PDF الذي وُقِّع أو أُضيفت إليه تعليقات أو عُدّل بطريقة أخرى بعد الإنشاء يحتوي على تحديثات تزايدية. كل تعديل يُلحِق قسم مراجع متقاطعة جديدًا وذيلًا ينتهي بعلامة %%EOF. يعمل RevisionExtractor بالكامل من خلال طرائق ساكنة فوق PdfReader مُحلَّل:
extractRevision(string $pdfData, PdfReader $reader, int $revision)يُعيد الملف مقتطعًا عند حدّ%%EOFللمراجعة المطلوبة. المراجعة0(الأحدث) تُعيد الملف بأكمله؛ والفهارس الأعلى تُعيد لقطات أقدم تدريجيًّا.getRevisionBoundaries(string $pdfData, PdfReader $reader)يُعيدlist<array{revision, startByte, endByte, sizeBytes}>يصف نطاق البايت الذي أسهمت به كل مراجعة.
هذا العزل متعمَّد. استخراج مراجعة أقدم يكشف فقط الكائنات المرئية حتى تلك النقطة، ما يحجب هجمات المراجع المتقاطعة الهجينة التي تُعيد فيها مراجعة لاحقة تعريف كائن سابق.
إرشاد عملي: فحص مراجعة
قسم بعنوان «إرشاد عملي: فحص مراجعة»يفحص هذا الإجراء سجلّ مراجعات ملف PDF قد يكون عُدّل بعد أن أنتجه Chrome. المثال مُهيّأ للإنتاج: يعلن الأنواع الصارمة، ويستعمل تلميحات نوع كاملة، ويتحقّق من مُدخَله، ويلتقط الاستثناء الأكثر تحديدًا.
- اقرأ بايتات PDF إلى الذاكرة، وارفض المُدخَل الفارغ قبل إنشاء القارئ.
- أنشئ
\NextPDF\Parser\PdfReaderواستدعِparse(). - اقرأ
getRevisionCount(). القيمة1تعني ملفًا بمراجعة واحدة دون تحديثات تزايدية. - لكل مراجعة، اقرأ
RevisionXRefTableالخاص بها وافحصgetActiveObjectCount()، وhasRootUpdate()، وgetSize(). - احسب نطاقات البايت لكل مراجعة باستخدام
RevisionExtractor::getRevisionBoundaries(). - التقط
PdfParseException، وهو أكثر استثناء محدّد يطرحه المحلِّل، وأظهِر رسالة تشخيصية.
<?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;}يرتّب القارئ المراجعات من الأحدث (index0) إلى الأقدم. لاستخراج لقطة أقدم واحدة بوصفها بايتات مستقلة، مثلًا لمقارنة ما غيّره تعديل، استدعِ المُستخرِج مباشرة:
<?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);}معالجة الأعطال
قسم بعنوان «معالجة الأعطال»يظهر كل عطل في المحلِّل بوصفه NextPDF\Artisan\Exception\PdfParseException. تحدّد الرسالة السبب. استعمل الجدول أدناه لربط جزء من الرسالة بالمرحلة التي أطلقته.
| جزء الرسالة | المرحلة | ماذا يعني |
|---|---|---|
missing %PDF- header | PdfReader::parse() | البايتات ليست PDF، أو أن المُدخَل اقتُطع في بدايته. |
Cannot find startxref marker / Invalid startxref offset | PdfReader::parse() | ذيل الملف معطوب، أو مؤشّر المراجع المتقاطعة خارج الحدود. |
Expected 'xref' keyword / Invalid xref subsection header | CrossRefParser::parseXRefTable() | جدول مراجع متقاطعة تقليدي مُشوَّه. |
XRef stream ... /Type /XRef / invalid /W array | CrossRefParser::parseXRefStream() | دفق مراجع متقاطعة تنقصه مدخلات قاموس مطلوبة. |
exceeds limit of (عدد xref أو دفق الكائنات) | CrossRefParser / PdfReader | عدد مزوَّر تخطّى حارس الحجب عن الخدمة. |
Unsupported stream filter | StreamDecoder::decode() | يستعمل الدفق مرشّحًا خارج المجموعة المدعومة FlateDecode / ASCIIHexDecode / ASCII85Decode. |
FlateDecode decompression failed / output exceeds ... bytes limit | StreamDecoder | البيانات المضغوطة غير صالحة أو تتمدّد بما يتجاوز سقف 16 MiB. |
Maximum nesting depth ... exceeded / Keyword exceeds maximum length | PdfTokenizer | بنية مُصاغة خصّيصًا أو مَرَضية تخطّت حدّ المُحوِّل. |
Page index ... not found / out of range in subtree | PdfReader::getPage() | فهرس الصفحة المطلوب غير موجود في شجرة الصفحات. |
Revision index ... out of range | PdfReader / RevisionExtractor | فهرس المراجعة خارج النطاق من 0 إلى getRevisionCount() - 1. |
عندما تلتقط الاستثناء، سجّل الرسالة ومسار المصدر، ثم أعِد طرحه أو أعِد خطأً معرَّفًا. لا تتجاهله بصمت. كتلة catch فارغة تُخفي المعلومة الوحيدة التي اجتهد المحلِّل لإنتاجها.
<?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;}الإعدادات الافتراضية الآمنة
قسم بعنوان «الإعدادات الافتراضية الآمنة»- استدعِ
parse()أولًا دائمًا. كل طريقة وصول علىPdfReaderتفترض أن سلسلة المراجع المتقاطعة محمَّلة. استدعاءgetObject()أوgetPage()قبلparse()لا يُعيد شيئًا مفيدًا. - عامِل المحلِّل بوصفه للقراءة فقط ومصمَّمًا لهيئة Chrome. يستهدف المجموعة الجزئية من بنية PDF التي يصدرها
printToPDFفي Chrome. ملفات PDF المشفّرة، وجداول التلميح الخطّية، والتحديثات التزايدية المتعارضة خارج النطاق بحكم التصميم. لا توسّعه ليصبح أداة إصلاح PDF عامة. - أبقِ حدود الأمان في مكانها. تحدّ سقوف التداخل، وطول الكلمات المفتاحية، وحجم المصفوفة، وعدد المراجع المتقاطعة، وفكّ الضغط من استهلاك الموارد على المُدخَل المعادي.
PdfParseExceptionالناتج عن تجاوز حدّ هو النتيجة الصحيحة لملف مُصاغ خصّيصًا. رفع حدٍّ لقبول مثل هذا الملف يوسّع سطح الهجوم. - اجعل الصفحة
0الافتراضية.getPage()وPageImporter::import()يُعيّنان أول صفحة افتراضيًّا. اختر فهرسًا آخر فقط حين يحتاج سير العمل إليه عمدًا. - تحقّق من المُدخَل قبل إنشاء القارئ. ارفض البايتات الفارغة أو غير القابلة للقراءة مبكّرًا، كما تفعل الأمثلة أعلاه، كي يظهر خطأ واضح على مستوى التطبيق قبل أي استثناء من المحلِّل.
- التقط
PdfParseException، لا\Exceptionالمجرّد أبدًا. إنه النوع الوحيد المحدَّد الذي يطرحه المحلِّل. التقاطه يمنع إخفاء الأعطال غير ذات الصلة.
انظر أيضًا
قسم بعنوان «انظر أيضًا»- دليل مطوّري Artisan — حدود الاستيراد فوق المحلِّل، بما في ذلك
ChromeHtmlRenderer، وPageImporter، وطبقات البنية. - مرجع واجهة Artisan البرمجية — جداول الطرائق المنشورة لسطح الحزمة العام.
- استكشاف أعطال Artisan — إرشاد قائم على الأعراض لأعطال العرض والاستيراد.
- إعداد عارض Chrome — ضبط العارض الذي يُنتِج ملفات PDF التي يقرأها هذا المحلِّل.
- ISO 32000-2:2020 §7.5 (بنية الملف، والمراجع المتقاطعة، والتحديثات التزايدية) و§7.2 (الاصطلاحات المعجمية) — المواصفة التي ينفّذها المحلّل المعجمي ومحلِّل المراجع المتقاطعة. راجع المعيار المنشور للاطّلاع على الصيغة الموثوقة على مستوى البايت.