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

دمج ملفات PDF خارجية أو إلحاق صفحات من مستندات قائمة

لديك عدة ملفات ⁨PDF⁩ على القرص، وتحتاج إلى ملف ⁨PDF⁩ واحد. تدمج هذه الوصفة المستندات القائمة من البداية إلى النهاية عبر سطح الدمج في ⁨Core⁩، وهو NextPDF\Document\PdfMerger. مرِّر سلاسل بايتات ⁨PDF⁩ خام. يعيد الدامج ترقيم كل كائن لتفادي التعارضات، ويبني شجرة صفحات واحدة وجدول مراجع متقاطعة واحدًا، ويُرجِع NextPDF\Document\MergeResult يمكنك كتابته إلى القرص أو بثُّه إلى عميل.

يغطّي السطح نفسه المهام الثلاث التي تحتاجها في الأغلب:

  • الدمج لقائمة مرتبة من ملفات ⁨PDF⁩ في مستند واحد.
  • الإلحاق لملف ⁨PDF⁩ ثانٍ بعد ملف ⁨PDF⁩ أساسي.
  • الإضافة في المقدمة للصفحات بوضع المستند الجديد أولًا في ترتيب الإدخال.

يجري الدمج داخل العملية، من دون متصفح بلا واجهة أو استدعاء شبكي. تحتاج إلى تثبيت ⁨Core⁩ (composer require nextpdf/core:^3) وملفّي ⁨PDF⁩ أو أكثر قابلين للقراءة.

Terminal window
composer require nextpdf/core:^3

ينظّم ملف ⁨PDF⁩ الصفحات في شجرة صفحات جذرها عقدة /Pages، ويحدّد موقع كل كائن غير مباشر عبر جدول مراجع متقاطعة. عند دمج مستندَي مصدر، تتداخل أرقام كائناتهما. يحتوي الملفان دائمًا تقريبًا على كائن 1 0 obj، و/Catalog، وعقدة /Pages. إذا اكتفيت بضمّ البايتات، فستنتج ملفًا تالفًا لأن المراجع لن تعود تشير إلى الكائنات التي تحدّدها.

يعالج PdfMerger هذا التداخل. يستخرج كائنات الصفحات من كل مدخل، ويعيد ترقيم كل كائن ضمن فضاء عناوين واحد، ويعيد كتابة مرجع /Parent لكل صفحة ليشير إلى عقدة /Pages مدمجة واحدة، ويُصدر كتالوجًا واحدًا وشجرة صفحات واحدة ومُذيِّلًا واحدًا. الناتج مستند جديد بنيويًا، لا مجرد ضمّ متتابع للبايتات.

قاعدة الترتيب بسيطة: تظهر الصفحات بالترتيب نفسه لملفاتها المصدرية في قائمة الإدخال. للإلحاق، ضع المستند الأساسي أولًا. وللإضافة في المقدمة، ضع المستند الجديد أولًا. لا توجد طريقة منفصلة للإضافة في المقدمة؛ فترتيب الإدخال هو عنصر التحكم الوحيد الذي تحتاجه.

يكشف new NextPDF\Document\PdfMerger() طريقتين.

  • يجمع merge(list<string> $pdfFiles, int $maxFiles = 100, int $maxTotalBytes = 200_000_000): MergeResult قائمة مرتبة من سلاسل بايتات ⁨PDF⁩ خام. يحدّ المُعامِلان المقيِّدان عدد الملفات وإجمالي حجم الإدخال. كلاهما يستخدم قيمًا افتراضية آمنة للإنتاج؛ شدِّدهما بحسب كل حِمل عمل.
  • append(string $basePdf, string $appendPdf): MergeResult مُغلِّف ميسِّر يدمج مستندين بالضبط بالترتيب. وهو مكافئ لـ merge([$basePdf, $appendPdf]).

تُرجِع الطريقتان كلتاهما NextPDF\Document\MergeResult، وهو كائن readonly يحمل $pdfData (البايتات المدمجة)، و$totalPages، و$sourceCount، و$mergedSize، والمساعد isValid() الذي يؤكّد أن الناتج يبدأ بترويسة %PDF.

المدخلات هي سلاسل بايتات خام، لا مسارات ملفات. اقرأ الملف بنفسك باستخدام file_get_contents()، أو اجلب البايتات من تخزين الكائنات. يُبقي ذلك الدامج خاليًا من افتراضات نظام الملفات، ويتيح لك دمج مستندات لا تلامس القرص أبدًا.

إذا احتجت إلى استيراد صفحة واحدة من ملف ⁨PDF⁩ خارجي بوصفها ⁨Form XObject⁩ قابلة لإعادة الاستخدام، مثلًا لختم صفحة ترويسة خلف محتوى مُولَّد، فاستخدم عقد المستورِد عبر الحزم NextPDF\Contracts\ImportedFormObjectInterface، المُنفَّذ في مستورِدات مثل nextpdf/artisan. ولتركيب مستند كامل بصفحات كاملة، استخدم سطح PdfMerger الموثَّق هنا.

عيّنة الشيفرة — البدء السريع

قسم بعنوان «عيّنة الشيفرة — البدء السريع»

تقرأ هذه العيّنة ملفين وتكتب الناتج المدمج. تترك معالجة الأخطاء خارجها لإظهار شكل الاستدعاء؛ أما العيّنة الإنتاجية أدناه فتضيف كل الضمانات.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Document\PdfMerger;
$merger = new PdfMerger();
$result = $merger->merge([
file_get_contents(__DIR__ . '/cover.pdf'),
file_get_contents(__DIR__ . '/body.pdf'),
file_get_contents(__DIR__ . '/appendix.pdf'),
]);
file_put_contents(__DIR__ . '/combined.pdf', $result->pdfData);
printf("Merged %d source(s) into %d page(s).\n", $result->sourceCount, $result->totalPages);

يبني هذا البرنامج المكتفي ذاتيًا مستندين صغيرين في الذاكرة، لذلك يعمل من دون ملف خارجي. يدمجهما، ويتحقق من النتيجة، ويكتب الناتج. يلتقط الاستثناءين اللذين يطلقهما سطح الدمج، ويعيد إطلاق كلٍّ منهما بسياق بدلًا من ابتلاعه. استبدل المدخلات الموجودة في الذاكرة بقراءاتك الخاصة عبر file_get_contents() أو بجلب من تخزين الكائنات، وأوصِل الناتج بطبقة الاستجابة أو التخزين لديك.

<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;
use NextPDF\Document\MergeResult;
use NextPDF\Document\PdfMerger;
use NextPDF\Exception\PageLayoutException;
use NextPDF\Exception\WriterException;
/**
* Build a tiny labelled PDF so the program is self-contained.
*
* In your own code, replace calls to this helper with reads of the external
* PDFs you want to combine, for example file_get_contents($path).
*/
function buildSample(string $label, int $pages): string
{
$doc = Document::createStandalone();
$doc->setTitle($label);
for ($page = 1; $page <= $pages; $page++) {
$doc->addPage();
$doc->setFont('helvetica', '', 12);
$doc->cell(0, 10, sprintf('%s - page %d', $label, $page), newLine: true);
}
return $doc->getPdfData();
}
// Validate the input set before touching the merger. An empty set is a
// configuration error, not an empty success.
/** @var list<string> $sources Raw PDF byte strings, in output order. */
$sources = [
buildSample('Cover', 1), // first in the list -> first in the output (prepend position)
buildSample('Body', 2),
buildSample('Appendix', 1), // last in the list -> appended after the body
];
if ($sources === []) {
throw new RuntimeException('No source PDFs supplied to merge.');
}
$merger = new PdfMerger();
try {
// Bound the merge deliberately: at most 50 files, 100 MB total input.
$result = $merger->merge($sources, maxFiles: 50, maxTotalBytes: 100_000_000);
} catch (PageLayoutException $e) {
// Raised when the list is empty or an input does not begin with %PDF.
throw new RuntimeException(
sprintf('Merge rejected an input: %s', $e->getConstraint()),
previous: $e,
);
} catch (WriterException $e) {
// Raised when the total input size exceeds the configured byte cap.
throw new RuntimeException(
sprintf('Merge exceeded its size budget at stage "%s".', $e->getWriterState()),
previous: $e,
);
}
if (!$result->isValid()) {
throw new RuntimeException('Merged output failed its structural header check.');
}
emitResult($result);
/**
* Write the merged document to the cookbook side-channel, or to a default file.
*/
function emitResult(MergeResult $result): void
{
printf(
"Merged %d source(s) into %d page(s), %d bytes.\n",
$result->sourceCount,
$result->totalPages,
$result->mergedSize,
);
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');
$path = $out !== false && $out !== '' ? $out : __DIR__ . '/combined.pdf';
if (file_put_contents($path, $result->pdfData) === false) {
throw new RuntimeException(sprintf('Could not write merged PDF to "%s".', $path));
}
}

الخرج القياسي المتوقَّع (إجمالي الصفحات هو مجموع أعداد صفحات المصادر، ويعتمد حجم البايتات على طريقة البناء):

Merged 3 source(s) into 4 page(s), <n> bytes.
  • المدخلات بايتات، لا مسارات. يأخذ merge() سلاسل ⁨PDF⁩ خام. اقرأ الملف بـ file_get_contents() أولًا. تمرير سلسلة مسار يجعل المدخل يفشل في فحص ترويسة %PDF ويُطلِق PageLayoutException.
  • الترتيب هو ترتيب الإخراج. تستقر الصفحات بالترتيب الذي تظهر به ملفاتها المصدرية في القائمة. لا توجد طريقة منفصلة للإضافة في المقدمة: ضع المستند الجديد أولًا للإضافة في المقدمة، أو أخيرًا للإلحاق.
  • القائمة الفارغة خطأ. قيمة $pdfFiles الفارغة تُطلِق PageLayoutException، لا نتيجة فارغة. تحقَّق من المجموعة قبل أن تستدعي الدامج.
  • كل مدخل يُتحقَّق منه مقدَّمًا. يجب أن يكون كل عنصر غير فارغ وأن يبدأ بـ %PDF. يُطلِق أول مدخل فاشل PageLayoutException مع القيد المنتهَك، ولا يُدمَج أي شيء.
  • الحدود تُطلِق استثناءً بدلًا من البتر. تجاوز maxFiles يُطلِق استثناءً عبر حارس الموارد الداخلي، وتجاوز maxTotalBytes يُطلِق WriterException. لا يُسقِط الدامج الملفات بصمت ولا يقتطع البايتات أبدًا، فاضبط الحدّين كليهما بحسب حِمل عملك.
  • الناتج جديد بنيويًا، لا ثابت على مستوى البايتات. يحمل المستند المدمج كتالوجًا وشجرة صفحات ومُذيِّلًا جديدة. تشغيلان على المدخلات نفسها يكونان متكافئين بنيويًا، لكن لا يُضمَن أن يكونا متطابقين على مستوى البايتات. لذلك تُعلِن هذه الوصفة ملف قابلية إعادة إنتاج structural.
  • التعليقات التوضيحية على مستوى الصفحة والموارد المشتركة. يركّب الدمج كائنات الصفحات في شجرة واحدة. البِنى على مستوى المستند التي تعيش خارج كائنات الصفحات في ملف مصدري لا تُنقَل. عندما تحتاج إلى استيراد صفحة واحدة بوصفها رسمًا قابلًا لإعادة الاستخدام مع موارده، استخدم مسار ImportedFormObjectInterface عبر مستورِد مثل nextpdf/artisan.

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

يجري الدمج داخل العملية؛ لا تغادر أي بايتات المستند المضيف، ولا يُجرى أي استدعاء شبكي. تعامَل مع كل ملف ⁨PDF⁩ خارجي كمدخل غير موثوق:

  • أبقِ الحدود مشدَّدة. maxFiles وmaxTotalBytes هما خطُّ دفاعك الأول ضد مدخلات حجب الخدمة. لأي سطح يقبل عمليات الرفع، اضبطهما على سقفك الحقيقي، لا على القيم الافتراضية السخيّة.
  • تحقَّق قبل أن تثق. نجاح الدمج يعني أن البايتات دُمِجت، لا أن المدخلات آمنة. مرِّر المدخلات غير الموثوقة عبر فاحص ⁨Core⁩ أولًا. راجع تحليل ملف ⁨PDF⁩ وفحصه للحصول على مسح فرز محدود يُظهر التشفير والتواقيع وعلامات الخطر قبل المعالجة الأثقل.
  • لا تُدرِج مدخلات المستخدم في مسار أبدًا. تكتب هذه الوصفة إلى مسار ثابت أو إلى القناة الجانبية لدفتر الوصفات. اشتقّ مسارات الإخراج من قيم يتحكم بها الخادم، لا من حقل طلب أبدًا، لتفادي اجتياز المسار.
  • لا أسرار في المستند. لا تُضمِّن بيانات اعتماد أو رموزًا أو معرِّفات داخلية في مستند مدمج تُرجِعه إلى عميل.

لا تطرح هذه الوصفة أي ادعاء معياري خاص بها. تركّب مستندات قائمة عبر سطح الدمج في ⁨Core⁩ وتتحقق من النتيجة بفحص ترويسة MergeResult::isValid(). نموذج شجرة الصفحات الذي يعيد PdfMerger بناءه هو بنية شجرة صفحات ⁨PDF 2.0⁩ الموصوفة في مرجع /modules/core/document/. للحصول على قراءة بنيوية لأي مستند مُدخَل أو مُخرَج، بما في ذلك الإصدار وعدد الصفحات والتشفير وأعلام التوقيع، استخدم فاحص ⁨Core⁩ الموثَّق في تحليل ملف ⁨PDF⁩ وفحصه.