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

المستند: أجزاء DPart، التقسيم / الدمج، وامتدادات المورِّد

تتعامل وحدة ⁨Document⁩ مع ملفات ⁨Portable Document Format⁩ ‏(⁨PDF⁩) كاملة، وليس مع محتوى الصفحات. فهي تبني التسلسل الهرمي لأجزاء المستند الذي تستخدمه سير العمل الخاضعة للتنظيم لإرفاق البيانات الوصفية، وتقسِّم ملف ⁨PDF⁩ إلى مقاطع حسب نطاقات الصفحات، وتدمج عدة ملفات ⁨PDF⁩ في ملف واحد، وتسجِّل امتدادات المطوِّرين في فهرس المستند.

Terminal window
composer require nextpdf/core:^3

تعمل هذه الوحدة فوق مستوى محتوى الصفحات. فبينما تُصدر وحدتا ⁨Graphics⁩ و⁨Content⁩ المعاملات، تعمل وحدة ⁨Document⁩ على المستوى البنيوي: أشجار الصفحات، وفهرس المستند، وشجرة أجزاء المستند.

جزء المستند (DPart) هو تقسيم منطقي لملف ⁨PDF.⁩ تحدِّد ⁨ISO 32000-2⁩ تسلسلاً هرمياً لأجزاء ⁨DPart⁩ تحمل عقده بيانات أجزاء المستند الوصفية (⁨DPM⁩). ويمكن لسير عمل خاضع للتنظيم، كسير عمل صيدلاني أو قانوني أو أرشيفي، أن يربط البيانات الوصفية بنطاق فرعي من الصفحات بدلاً من ربطها بالملف كاملاً — §14.12. DPart عقدة ثابتة من نوع readonly: تشير العقدة الطرفية إلى تسلسل متجاور من فهارس الصفحات، وتجمع العقدة الوسيطة عقد DPart الأبناء في شجرة. DPartRoot هو جذر الشجرة الذي يسلسله الكاتب ⁨Writer.⁩ ومدخلا /Start و/End في العقدة الطرفية مرجعان غير مباشرين إلى كائنات الصفحات، وليسا عددين صحيحين لفهرس الصفحة — §14.12. يحلّ DPart::resolveWithPageObjects() تلك المدخلات مقابل خريطة فهرس الصفحة→رقم الكائن التي يوفِّرها الكاتب، ويُعيد صيغة مرجع /Start/End الاختياري). ولا يلجأ إلى صيغة العدد الصحيح إلا في مسارات الاختبار عندما لا تكون الخريطة متوفرة.

PdfMerger وPdfSplitter هما واجهة تأليف المستندات. يدمج PdfMerger كائنات الصفحات من عدة ملفات ⁨PDF⁩ مدخلة، ويعيد ترقيم الكائنات لتجنب التعارضات، ثم يعيد بناء شجرة صفحات واحدة وجدول مرجعية متقاطعة واحد. شجرة الصفحات التي ينتجها هي عقدة Pages متوازنة تحتوي Kids وCount، إضافةً إلى نموذج السمات القابلة للوراثة الذي يحدِّده ⁨PDF⁩ لعقد شجرة الصفحات — §7.7.3. أما PdfSplitter فينفِّذ العملية العكسية: إذ يستخرج نطاقات الصفحات في كائنات SplitDocument مستقلة. PageRange هو كائن القيمة الذي يستخدمه كلا الصنفين. يبدأ ترقيمه من 1، ويتحقق من حدوده، ويجيب عن contains() وcount() وtoArray().

تُنمذِج VendorExtensionRegistry وExtensionsDictionary وDeveloperExtensionEntry قاموس امتدادات المطوِّرين في فهرس المستند. يستخدم المحرك ذلك القاموس للإعلان عن مستوى امتداد للمورِّد يتجاوز المواصفة الأساسية. ويرفض السجل عمليات إعادة التسجيل المتعارضة لنفس بادئة المورِّد عبر VendorExtensionRegistryConflictException. كما تُنمذِج CollectionDictionary وCollectionSort مدخل فهرس مجموعة ⁨PDF⁩ (مجموعة محمولة أو حافظة).

الصنفالطرائق الرئيسيةالدور
DPartisLeaf(), hasMetadata(), resolveWithPageObjects(), write()عقدة جزء مستند ثابتة (@since 1.12.0)
DPartRootisEmpty(), write()جذر شجرة ⁨DPart⁩ الذي يسلسله الكاتب ⁨Writer⁩ (@since 1.12.0)
PdfMergermerge(array $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000), append()دمج عدة ملفات ⁨PDF⁩ مع إعادة ترقيم الكائنات (@since 1.9.0)
PdfSplittersplit(), splitEvery(), extractPages()تقسيم بحسب نطاق الصفحات إلى SplitDocument (@since 1.9.0)
PageRangecontains(int $page), count(), toArray()كائن قيمة لنطاق الصفحات يبدأ ترقيمه من 1
MergeResult / SplitResultisValid(), count(), document(), totalOutputSize()كائنات نتيجة التأليف
VendorExtensionRegistryتسجيل الامتداداتسجل امتدادات المطوِّرين (@since 2.2.0)
ExtensionsDictionarywithEntry(), entries(), isEmpty(), toPdfDictionary()باني قاموس امتدادات ثابت (@since 2.0.0)
CollectionDictionarytoPdfDictionary()مدخل فهرس لمجموعة محمولة (@since 2.0.0)

شغِّل composer docs:generate-api-php -- --module=Document لتوليد جدول ⁨PHPDoc⁩ الكامل.

قسِّم ملف ⁨PDF⁩ إلى مستندات من صفحة واحدة، ثم افحص النتيجة.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PageRange;
use NextPDF\Document\PdfSplitter;
$splitter = new PdfSplitter();
$result = $splitter->splitEvery(file_get_contents('/srv/in/report.pdf'), 1);
foreach (range(0, $result->count() - 1) as $index) {
$segment = $result->document($index);
file_put_contents("/srv/out/page-{$index}.pdf", $segment->pdfData);
}
$singlePage = $splitter->extractPages(
file_get_contents('/srv/in/report.pdf'),
new PageRange(2, 4),
);

ادمج عدة ملفات ⁨PDF⁩ ضمن ميزانية إدخال صريحة، ثم تحقَّق من صحة النتيجة قبل كتابة المخرَج المجمَّع.

<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Document\PdfMerger;
use NextPDF\Exception\PageLayoutException;
/** @var list<string> $sources Raw PDF byte strings to combine. */
$sources = array_map(
static fn (string $path): string => file_get_contents($path),
glob('/srv/batch/*.pdf') ?: [],
);
$merger = new PdfMerger();
try {
// Bound the merge: at most 50 files, 100 MB total.
$merged = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);
} catch (PageLayoutException $e) {
throw new \RuntimeException('Merge rejected: empty or invalid input set.', previous: $e);
}
if (!$merged->isValid()) {
throw new \RuntimeException('Merged document failed structural validation.');
}
file_put_contents('/srv/out/combined.pdf', $merged->pdfData);
  • يفرض PdfMerger::merge() وPdfSplitter::split() حدود الإدخال عبر ResourceGuard. فالمدخلات التي تحتوي ملفات أو بايتات أكثر من اللازم تثير استثناءً بدلاً من اقتطاعها بصمت. اضبط maxFiles / maxTotalBytes عمداً بما يناسب حملك التشغيلي.
  • قائمة ملفات فارغة أو قائمة نطاقات فارغة تثير PageLayoutException. عامِل هذه الحالات كأخطاء في التهيئة، لا كنتائج فارغة.
  • يبدأ ترقيم PageRange من 1 وهو شامل للحدين. أما قائمة pages في عقدة DPart الطرفية فهي فهارس صفحات يبدأ ترقيمها من 0. يستخدم التجريدان أساسَي ترقيم مختلفين للفهارس. حوِّل صراحةً عند الانتقال بينهما.
  • DPart من نوع readonly. لبناء شجرة مختلفة، أنشئ عقداً جديدة بدلاً من تعديل عقدة قائمة. لا يُعيد resolveWithPageObjects() صيغة الاحتياط القائمة على فهرس العدد الصحيح إلا عندما تكون خريطة كائنات الصفحات فارغة. لا تعتمد على ذلك المسار في مخرَج بيئة الإنتاج.
  • يثير VendorExtensionRegistry الاستثناء VendorExtensionRegistryConflictException عند تكرار بادئة المورِّد. سجِّل كل بادئة مرة واحدة فقط.

يتوسع التقسيم والدمج خطياً مع عدد الصفحات، وتتحكم فيهما عملية التحليل وإعادة ترقيم الكائنات، لا عمليات التخزين المؤقت الداخلية للوحدة نفسها. يندرج الحمل المرجعي الافتراضي ضمن ميزانية قدرها 1500 ⁨ms⁩ زمن جداري / 64 ⁨MB⁩ ذروة. عمليات الدمج الكبيرة مقيَّدة أساساً بإجمالي بايتات الإدخال. يُبقي حارس maxTotalBytes ذروة الذاكرة محدودة. ملف تعريف قابلية إعادة الإنتاج هو structural: يحمل ملف ⁨PDF⁩ المدموج أو المقسَّم مذيِّلاً جديداً و/ID جديداً، فيكون تشغيلان متساويين بنيوياً لكن غير متطابقين على مستوى البايت.

يستهلك PdfMerger::merge() وPdfSplitter::split() بايتات ⁨PDF⁩ غير موثوقة. قبل التحليل، يمرِّر كلاهما الإدخال عبر ResourceGuard::assertSize() / assertCount()، مما يحدّ من هجوم حجب الخدمة الناتج عن تضخيم فك الضغط أو تضخيم عدد الكائنات. أبقِ وسائط maxFiles وmaxTotalBytes وmaxBytes ضيقة بما يناسب النشر بدلاً من الاعتماد على القيم الافتراضية. عامِل كل ملف ⁨PDF⁩ مُدخَل على أنه معادٍ. عندما تكون المصادر مقدَّمة من المستخدم، شغِّل التأليف الدُّفعي في عامل مقيَّد. راجع نموذج تهديد المحرك في /modules/core/security/ لمعرفة حدود الثقة.

تتبع شجرة ⁨DPart⁩ التي تبنيها هذه الوحدة نموذج جزء المستند في ⁨ISO 32000-2⁩ §14.12، مع إصدار مدخلَي /Start و/End الطرفيين كمراجع غير مباشرة إلى كائنات الصفحات بموجب البند نفسه. يستخدم المخرَج المدموج بنية عقدة شجرة الصفحات المحدَّدة في §7.7.3. هذه حقائق تنفيذية ينتجها src/Document/ وتختبرها tests/Unit/Document/ (DPartTest، DPartRootTest، DPartPageRefTest، DocumentPdfMergerDeepTest، DocumentPageRangeParseDeepTest). وهي ليست إقراراً بالمطابقة الكاملة لـ ⁨PDF 2.0⁩ من طرف إلى طرف. تُتحقَّق مطابقة المستند الكامل بواسطة مجموعتي الأوراكل والملفات الذهبية الموصوفتين في /modules/core/conformance/.