مقاله فنی

ادغام چندین فایل PDF در یک سند با PDFium Component

PDFium Component ادغام 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 XObjectها همگی به‌عنوان بخشی از جریان محتوای صفحه منتقل می‌شوند. حاشیه‌نویسی‌های سطح صفحه، از جمله یادداشت‌ها، هایلایت‌ها و خطوط ترسیمی نیز منتقل می‌شوند، زیرا در فرهنگ‌نامه صفحه ذخیره شده‌اند، نه در سطح سند

فراداده سطح سند وضعیت متفاوتی دارد. رشته‌های عنوان، نویسنده، موضوع و کلیدواژه در Info dictionary منبع منتقل نمی‌شوند. سند مقصد پس از CreateDocument با فراداده خالی آغاز می‌شود؛ بنابراین اگر این فیلدها باید در خروجی ادغام‌شده مقدار داشته باشند، لازم است پیش از فراخوانی SaveAs آن‌ها را مستقیماً روی PdfDest تنظیم کنید. ویژگی‌های Title، Author، Subject، Keywords و Creator در TPdf رشته‌های ساده می‌پذیرند و هنگام ذخیره آن‌ها را در Info dictionary می‌نویسند

فیلدهای فرم تعاملی پیچیده‌تر هستند. تعریف فیلدهای AcroForm در یک فرهنگ‌نامه سطح سند قرار دارد، نه در جریان محتوای هر صفحه. وقتی ImportPages صفحه‌ای دارای فیلد فرم را کپی می‌کند، ظاهر بصری فیلدها منتقل می‌شود، زیرا در جریان محتوای صفحه رندر شده است؛ اما ویجت‌هایی که آن‌ها را تعاملی می‌کنند، بخشی از ساختار AcroForm هستند و منتقل نمی‌شوند. در یک ادغام معمولی، فیلد متنی سند منبع مقدار زمان وارد کردن را نمایش می‌دهد، اما در فایل ادغام‌شده قابل ویرایش نخواهد بود. اگر لازم است فیلدها قابل تکمیل باقی بمانند، پیش از وارد کردن صفحات آن‌ها را در هر سند منبع flatten کنید. این کار مقادیر فعلی را در جریان محتوا تثبیت و لایه تعاملی را حذف می‌کند و بدون ایجاد ویجت‌های خراب در خروجی، نتیجه‌ای بصری و تمیز به دست می‌دهد

فایل‌های منبع رمزگذاری‌شده

اسناد منبع محافظت‌شده با گذرواژه مانند فایل‌های بدون رمزگذاری باز می‌شوند، با این تفاوت که ابتدا باید یک ویژگی اضافی را تنظیم کنید. پیش از اجرای Active := True گذرواژه را به PdfSrc.Password اختصاص دهید تا 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 را ایجاد می‌کند که در فایل مفقود دیده می‌شود؛ بنابراین بررسی صریح در اینجا نیز ضروری است. رمزگذاری به مقصد منتقل نمی‌شود و صفحات واردشده از منبع محافظت‌شده در مقصد به‌صورت محتوای بدون محافظت قرار می‌گیرند. اگر خروجی ادغام‌شده نیز باید رمزگذاری شود، پیش از فراخوانی SaveAs آن را روی PdfDest پیکربندی کنید

ذخیره نتیجه

متد SaveAs در TPdf یک مسیر فایل یا یک TStream را می‌پذیرد. برای بیشتر عملیات ادغام، overload مربوط به فایل گزینه مناسب است:

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

آرگومان دوم اختیاری از نوع TSaveOption حالت ذخیره را کنترل می‌کند. مقدار پیش‌فرض saNone در صورت بارگذاری سند از فایل، یک به‌روزرسانی افزایشی می‌نویسد و اگر سند از ابتدا ایجاد شده باشد، بازنویسی کامل انجام می‌دهد. از آنجا که مقصد ساخته‌شده با CreateDocument همیشه تازه است، خروجی یک فایل فشرده با یک revision خواهد بود. آرگومان سوم از نوع TPdfVersion امکان تعیین نسخه سربرگ PDF را برای مصرف‌کنندگان پایین‌دستی فراهم می‌کند که به نسخه مشخصی نیاز دارند؛ باقی گذاشتن آن روی pvUnknown به PDFium اجازه می‌دهد نسخه را بر اساس محتوا انتخاب کند

متدهای ImportPages و SaveAs که در این مقاله نشان داده شده‌اند، بخشی از PDFium Component برای Delphi و C++Builder هستند