تخطَّ إلى المحتوى

البيانات الوصفية: بناء حزمة XMP وقراءتها تدفقيًا

وحدة البيانات الوصفية هي طبقة منصّة البيانات الوصفية القابلة للتوسعة (⁨XMP⁩) في المحرّك. فهي تبني حزمة ⁨XMP⁩ التي يحملها ملف بصيغة المستندات المحمولة (⁨PDF⁩) بوصفها دفق بيانات وصفية، وتقرأ حزمة موجودة من دون تحميل المستند كاملًا في الذاكرة، وتُصدِر امتداد ⁨XMP⁩ لسجلّ تدقيق المحرّك.

Terminal window
composer require nextpdf/core:^3

يخزّن ⁨PDF⁩ البيانات الوصفية على مستوى المستند بوصفها حزمة ⁨XMP⁩ داخل دفق بيانات وصفية مرفق بفهرس المستند، كما يوضّح ⁨ISO 32000-2⁩ §14.3. تتولّى هذه الوحدة إنتاج تلك الحزمة واستهلاكها. واجهتها صغيرة ومركّزة عن قصد: ثلاثة أصناف ضمن NextPDF\Metadata\Xmp.

ينتج XmpMetadataBuilder الحزمة. فهو يسلسل مجموعة خصائص في مستند ⁨XMP⁩ صحيح البنية ومغلّف بتعليمات المعالجة القياسية <?xpacket?>. ويستخدم المعرّف الفريد عالميًا (⁨GUID⁩) المعياري للحزمة وعلامة ترتيب البايتات اللذين تحدّدهما مواصفة ⁨XMP.⁩ الناتج هو سلسلة البايتات التي يضمّنها ⁨Writer⁩ بوصفها دفق البيانات الوصفية، أي تمثيل ⁨XMP⁩ داخل ⁨PDF⁩ الموصوف في §14.3.

يستهلك XmpStreamReader الحزمة. وهو مصمَّم للتعامل مع المدخلات العدائية. يُقرأ المصدر على شكل أجزاء بحجم 64 ⁨KB⁩ إلى ملف مؤقت محدود قبل التحليل. ويفرض القارئ حدًّا أقصى إجماليًا للبايتات أثناء تلك الكتابة. يُضبَط محمّل الكيانات في ⁨libxml⁩ على ⁨null⁩ أثناء التحليل ثم يُستعاد بعده. يؤدّي وجود ⁨DOCTYPE⁩ إلى رفض صارم. يُعيد iterateProperties() مولّدًا يُنتج صفوف (namespaceUri, localName, textContent) لكل عنصر طرفي من دون بناء الشجرة كاملة في الذاكرة؛ ولا يبقى مقيمًا في المحلّل في أي لحظة إلا العنصر الحالي وعقدة نصّه. تُطلِق الحزمة المفرطة الحجم PacketTooLargeException؛ بينما تُطلِق لغة الترميز القابلة للتوسعة (⁨XML⁩) المشوّهة، أو وجود ⁨DOCTYPE⁩، أو المدخلات غير المرمّزة بـ ⁨UTF-8⁩ الاستثناء InvalidConfigException.

يمثّل XmpAuditFieldEmitter الامتداد الخاص بالمحرّك. فهو يصيّر AuditReport في حقل ⁨XMP⁩ مخصّص ضمن مساحة الأسماء nextpdfAudit، بحيث يرافق تدقيق مطابقة المستند الملفَ بوصفه ⁨XMP⁩ متوافقًا مع المعايير، بدلًا من أن يكون ملفًا جانبيًا. إنّ AuditReport الذي يصيّره ليس من إنتاج المُصدِر. يفعّل المستدعي الإثراء بتشغيل عملية تصيير في وضع CssRenderingMode::Audit مع auditCollector يوفّره المستدعي ويُهيَّأ عبر Config(auditCollector: ...). المجمّع مدفوع من المستدعي: فالمستدعي يغذّيه، والمُصدِر يصيّر كل ما جمعه. وهو أحدث من واجهة ⁨XMP⁩ الأساسية (@since 5.4.0). أمّا الباني والقارئ فهما @since 2.0.0.

الصنفالأعضاء الرئيسيونالدور
XmpMetadataBuilderbuild(): string، XPACKET_GUID، XPACKET_BOMيسلسل مجموعة خصائص في حزمة ⁨XMP⁩ ‏(@since 2.0.0)
XmpStreamReaderiterateProperties(mixed $source, int $byteCap = DEFAULT_BYTE_CAP): \Generator، DEFAULT_BYTE_CAPقارئ ⁨XMP⁩ محدود وتدفقي ويرفض ⁨DOCTYPE⁩ ‏(@since 2.0.0)
PacketTooLargeExceptionيمتدّ من NextPdfExceptionيُطلَق عندما تتجاوز حزمة ⁨XMP⁩ الحدّ الأقصى للبايتات (@since 2.0.0)
XmpAuditFieldEmitterrender(?AuditReport $report): string، NAMESPACE_URIيصيّر سجلّ التدقيق بوصفه حقل ⁨XMP⁩ مخصّص (@since 5.4.0)

شغّل composer docs:generate-api-php -- --module=Metadata لإنشاء جدول ⁨PHPDoc⁩ الكامل.

اقرأ الخصائص تدفقيًا من حزمة ⁨XMP⁩ موجودة ضمن حدّ أقصى صريح للبايتات.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Metadata\Xmp\XmpStreamReader;
$reader = new XmpStreamReader();
foreach ($reader->iterateProperties(file_get_contents('/srv/in/xmp.xml'), byteCap: 1_048_576) as [$ns, $name, $value]) {
printf("%s:%s = %s\n", $ns, $name, $value);
}

اقرأ الحزمة بأسلوب دفاعي، وحوّل إخفاقات الوحدة المُنمَّطة إلى نتيجة على مستوى التطبيق بدلًا من ترك أعطال المحلّل الخام تنفلت.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Exception\InvalidConfigException;
use NextPDF\Metadata\Xmp\PacketTooLargeException;
use NextPDF\Metadata\Xmp\XmpStreamReader;
use Psr\Log\LoggerInterface;
final readonly class XmpIngestService
{
public function __construct(
private XmpStreamReader $reader,
private LoggerInterface $logger,
) {}
/**
* @param resource|string $source A stream resource or XMP byte string.
*
* @return array<string, string> Flattened "ns:name" => value map.
*/
public function ingest(mixed $source): array
{
$properties = [];
try {
// Cap untrusted XMP at 4 MB regardless of the 1 GiB default.
foreach ($this->reader->iterateProperties($source, byteCap: 4_194_304) as [$ns, $name, $value]) {
$properties["{$ns}:{$name}"] = $value;
}
} catch (PacketTooLargeException $e) {
$this->logger->warning('XMP packet exceeded ingest cap; rejected.', ['error' => $e->getMessage()]);
return [];
} catch (InvalidConfigException $e) {
$this->logger->warning('XMP packet malformed or unsafe; rejected.', ['error' => $e->getMessage()]);
return [];
}
return $properties;
}
}
  • يرفض XmpStreamReader أي ⁨DOCTYPE⁩ رفضًا قاطعًا. هذا دفاع ضدّ هجوم الكيان الخارجي في ⁨XML⁩ ‏(⁨XXE⁩)، وليس تحسينًا تجميليًا للتحقق؛ فالحزمة التي تحتاج إلى ⁨DOCTYPE⁩ لا تُقبَل. طهّرها في مرحلة سابقة.
  • يبلغ الحدّ الأقصى للبايتات افتراضيًا 1 ⁨GiB⁩ ‏(DEFAULT_BYTE_CAP). هذا الافتراضي سقف، وليس توصية. مرّر byteCap أضيق للمدخلات غير الموثوقة.
  • iterateProperties() مولّد. استهلكه مرة واحدة؛ فالمرور عليه مرتين لا يُعيد التشغيل.
  • يضبط القارئ محمّل الكيانات في ⁨libxml⁩ على ⁨null⁩ أثناء التحليل ثم يستعيده. لا تشغّله بالتزامن مع أي تحليل آخر قائم على ⁨libxml⁩ في الطلب نفسه إذا كان ذلك التحليل يعتمد على محمّل الكيانات.
  • إنّ XmpAuditFieldEmitter::render(null) صالح ويُنتج تصييرًا فارغًا؛ كما أنّ AuditReport الذي تكون قيمته ⁨null⁩ يعني “لا تدقيق”، لا خطأً.

الباني خطّي بالنسبة إلى عدد الخصائص. يُهيمن أطول نصّ متّصل مفرد على استهلاك القارئ للذاكرة، لا حجم المستند، لأنّ العنصر الحالي وحده يبقى مقيمًا في المحلّل؛ فالحزم الكبيرة تُقرأ تدفقيًا بدلًا من تحميلها في الذاكرة. يقع حمل العمل المرجعي الافتراضي ضمن ميزانية قدرها 1500 ⁨ms⁩ زمن جداري / 64 ⁨MB⁩ ذروة. سمة قابلية إعادة الإنتاج هي structural: فحزمة ⁨XMP⁩ تسجّل طوابع زمنية للتعديل. قد يختلف بناءان للبيانات الوصفية المنطقية نفسها في تلك الحقول، بينما تبقى بنيتهما متطابقة.

يحلّل XmpStreamReader ⁨XML⁩ غير موثوق ومحصَّن وفقًا لذلك. يقيّد التقطيع التدفقي مع حدّ أقصى مفروض للبايتات هجوم حجب الخدمة القائم على تضخيم الذاكرة. رفض ⁨DOCTYPE⁩ يسدّ ثغرة ⁨XXE.⁩ يحجب LIBXML_NONET استعلام الكيانات عبر الشبكة. تُرفَض المدخلات غير المرمّزة بـ ⁨UTF-8.⁩ ومع ذلك اضبط byteCap بما يلائم النشر لأي حزمة مصدرها خارجي، بدلًا من الاعتماد على الافتراضي البالغ غيغابايت. عامِل قيم خصائص ⁨XMP⁩ بوصفها سلاسل غير موثوقة عند عودتها إلى التطبيق. راجع نموذج تهديدات المحرّك في /modules/core/security/.

الحزمة التي ينتجها XmpMetadataBuilder هي تمثيل دفق البيانات الوصفية ⁨XMP⁩ داخل ⁨PDF⁩ المعرّف في ⁨ISO 32000-2⁩ §14.3 (). أمّا صيغة تسلسل ⁨XMP⁩ نفسها فتحكمها مواصفة ⁨XMP⁩ ‏(⁨ISO 16684-1⁩)، وهي ليست ضمن مجموعة الاستشهادات القابلة للتحقق. يُشار إلى ذلك المتطلب بالرقم، لا بتثبيت مقطعي. هذه حقائق تنفيذية ينتجها src/Metadata/Xmp/ وتختبرها tests/Unit/Metadata/Xmp/. يُتحقّق من مطابقة البيانات الوصفية من الطرف إلى الطرف لأي ملف تعريف (⁨PDF/A⁩، ⁨PDF/UA⁩) عبر مجموعات الأوراكل والمجموعات الذهبية الموصوفة في /modules/core/conformance/.