مقالة تقنية

دمج ملفات PDF متعددة في مستند واحد باستخدام PDFium VCL

يوفر PDFium VCL دمج ملفات PDF من خلال طريقة واحدة فقط: ImportPages. النمط ثابت دائمًا: أنشئ مستند الوجهة فارغًا، وافتح كل ملف مصدر، ثم استدعِ ImportPages لنسخ الصفحات، وأغلق المصدر، وكرر العملية. عندما تنتهي الحلقة، يكتب SaveAs النتيجة إلى القرص. لا يوجد وضع دمج خاص ولا إعدادات يجب تبديلها. التعقيد يكمن في الحالات الحدية، وهناك بعض الحالات التي تفاجئك من دون إنذار

الحلقة الأساسية

تكفيك مثيلتان من TPdf. إحداهما تحتفظ بمستند الوجهة، وتنشأ فارغة باستخدام CreateDocument. والأخرى تفتح كل ملف مصدر على التوالي. فيما يلي إجراء يأخذ قائمة من مسارات الملفات ويكتب الناتج المدمج إلى مسار واحد:

procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
  PdfDest, PdfSrc: TPdf;
  InsertAt, I: Integer;
begin
  PdfDest := TPdf.Create(nil);
  PdfSrc  := TPdf.Create(nil);
  try
    PdfDest.CreateDocument;
    InsertAt := 1;  // ImportPages uses 1-based destination position

    for I := 0 to FileList.Count - 1 do
    begin
      PdfSrc.FileName := FileList[I];
      PdfSrc.Active   := True;

      if not PdfSrc.Active then
        raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);

      PdfDest.ImportPages(
        PdfSrc,
        '1-' + IntToStr(PdfSrc.PageCount),  // full document range
        InsertAt);

      Inc(InsertAt, PdfSrc.PageCount);
      PdfSrc.Active := False;
    end;

    PdfDest.SaveAs(OutputPath);
  finally
    PdfSrc.Free;
    PdfDest.Free;
  end;
end;

هناك أمران يسهل إغفالهما في هذا المثال عند القراءة الأولى. الأول هو كيفية إبلاغ PDFium عن أخطاء التحميل. إن Active := True لا يرفع استثناء أبدًا، فإذا كان الملف مفقودًا أو تالفًا أو محميًا بكلمة مرور، فإن PDFium يلتقط الخطأ داخليًا ويترك Active بقيمة False. من دون الفحص الصريح في السطر 10، سيخرج الملف التالف بصمت من عملية الدمج من دون أي إشارة في الناتج. وسيكون ملف PDF النهائي أقل صفحات من المتوقع ولن تعرف أي ملف كان السبب

الأمر الثاني هو العداد InsertAt. الوسيط الثالث لـ ImportPages هو الموضع المعتمد على 1 في مستند الوجهة حيث تهبط أول صفحة مستوردة. البدء من 1 يضع المستند المصدر الأول في بداية ملف فارغ أصلًا. وبعد كل مصدر، يتقدم العداد بمقدار PdfSrc.PageCount، بحيث تضاف الدفعة التالية من الصفحات بعد الأخيرة. إذا نسيت زيادة العداد، فسيكتب كل مصدر لاحق فوق الصفحات في الموضع 1، ولن تحصل إلا على المستند الأخير في القائمة

نطاقات الصفحات المحددة

لا يلزمك أخذ كل صفحة من المصدر. سلسلة النطاق الممررة كوسيط ثانٍ تتبع تنسيقًا بسيطًا من الفواصل والشرطات: "1-3" يأخذ الصفحات من 1 إلى 3، و"2,4,6" يختار ثلاث صفحات محددة، و"1-" يعني من الصفحة 1 حتى نهاية المستند. يمكن دمج النطاقات في سلسلة واحدة، لذا فإن "1-3,5,7-" يتجاوز الصفحتين 4 و6. توجد هنا نقطة دقيقة مهمة: الأرقام تشير دائمًا إلى الصفحات في مستند المصدر، بدءًا من 1، بغض النظر عن المكان الذي تنتهي إليه تلك الصفحات في الوجهة. إذا أردت الصفحات من 40 إلى 50 من كتالوج من 200 صفحة، فالسلسلة هي "40-50"، لا موضعًا نسبيًا إلى ما هو موجود بالفعل في الوجهة

// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active   := True;
if PdfSrc.Active then
begin
  // Page 1 is the cover; pages 3-5 are the summary
  PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
  Inc(InsertAt, 4);  // 1 cover + 3 summary pages = 4 pages added
  PdfSrc.Active := False;
end;

عند حساب الزيادة في InsertAt، احسب الصفحات التي استوردتها فعليًا، لا العدد الكلي لصفحات المصدر. إذا مررت '1,3-5' فأنت استوردت 4 صفحات، لذا زد العداد بمقدار 4. أما الزيادة بمقدار PdfSrc.PageCount فستترك فجوة من المواضع الفارغة في الوجهة وتضع مستند المصدر التالي أبعد داخل الملف مما هو مقصود

ما الذي يحافظ عليه ImportPages وما الذي لا يحافظ عليه

الصفحات المنسوخة بواسطة ImportPages تحتفظ بمحتواها المرئي كما هو. النصوص، والرسوم المتجهة، والصور النقطية، والخطوط المضمنة، وكائنات النماذج form XObjects كلها تنتقل ضمن مسارات محتوى الصفحة. كما تنتقل التعليقات التوضيحية على مستوى الصفحة، بما في ذلك التعليقات والتظليلات وضربات الحبر، لأنها مخزنة داخل قاموس الصفحة لا على مستوى المستند

أما البيانات الوصفية على مستوى المستند فقصتها مختلفة. تبقى قيم العنوان والمؤلف والموضوع والكلمات المفتاحية في قاموس Info الخاص بالمصدر في مكانها. يبدأ مستند الوجهة ببيانات وصفية فارغة بعد CreateDocument، لذا إذا كان الناتج المدمج يحتاج إلى هذه الحقول فعليك تعيينها مباشرة على PdfDest قبل استدعاء SaveAs. خصائص Title وAuthor وSubject وKeywords وCreator في TPdf تستقبل سلاسل عادية وتكتب في قاموس Info عند الحفظ

حقول النماذج التفاعلية أكثر تعقيدًا. تعيش تعريفات حقول AcroForm في قاموس على مستوى المستند لا داخل مسارات الصفحات الفردية. عندما ينسخ ImportPages صفحة تحتوي على حقول نموذج، ينتقل المظهر المرئي لتلك الحقول لأنّه يُرسم داخل مسار محتوى الصفحة، لكن عناصر التحكم في الحقول التي تجعلها تفاعلية هي جزء من بنية AcroForm ولا تنتقل. في دمج نموذجي، سيعرض حقل نص من مستند مصدر القيمة التي كانت له وقت الاستيراد، لكنه لن يكون قابلًا للتحرير في الملف المدمج. إذا كنت تحتاج إلى بقاء الحقول قابلة للتعبئة، فقم بتسطيحها في كل مستند مصدر قبل الاستيراد. عندها تُدمج القيم الحالية في مسار المحتوى وتُزال الطبقة التفاعلية، فتحصل على نتيجة مرئية نظيفة من دون عناصر تحكم معطلة في الناتج

ملفات المصدر المشفرة

تفتح المستندات المصدر المحمية بكلمة مرور بالطريقة نفسها التي تُفتح بها المستندات غير المشفرة، مع خاصية إضافية واحدة يجب ضبطها أولًا. عيّن كلمة المرور على PdfSrc.Password قبل تبديل Active := True، وسيستخدمها PDFium أثناء الفتح:

PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active   := True;
if not PdfSrc.Active then
  raise Exception.Create('Wrong password or file cannot be opened');

PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;

تؤدي كلمة المرور الخاطئة إلى النتيجة الصامتة نفسها Active = False كما في الملف المفقود، لذا فإن الفحص الصريح ضروري هنا بالقدر نفسه. التشفير لا ينتقل إلى الوجهة: فالصفحات المستوردة من مصدر محمي تصل إلى الوجهة على أنها محتوى غير مشفر. وإذا كان الناتج المدمج يحتاج أيضًا إلى التشفير، فقم بتهيئته على PdfDest قبل استدعاء SaveAs

حفظ النتيجة

SaveAs في TPdf يقبل إما مسار ملف أو TStream. وفي معظم عمليات الدمج، تكون النسخة التي تحفظ إلى ملف هي المطلوبة:

PdfDest.SaveAs('merged-output.pdf');

الوسيط الثاني الاختياري هو TSaveOption الذي يتحكم في نمط الحفظ. القيمة الافتراضية saNone تكتب تحديثًا تزايديًا إذا كان المستند محمّلًا من ملف أو إعادة كتابة كاملة إذا كان قد أُنشئ حديثًا. وبما أن الوجهة المبنية باستخدام CreateDocument تكون جديدة دائمًا، فسيكون الناتج ملفًا مضغوطًا ذا مراجعة واحدة. أما الوسيط الثالث TPdfVersion فيتيح لك تثبيت رأس إصدار PDF عندما يكون لديك مستهلكون لاحقون يحتاجون إلى إصدار محدد، وتركه على pvUnknown يسمح لـ PDFium بالاختيار وفقًا للمحتوى

الطريقتان ImportPages وSaveAs المعروضتان هنا هما جزء من مكوّن PDFium VCL لـ Delphi وC++Builder