Технічна стаття

Об'єднання кількох файлів 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. Без явної перевірки пошкоджений файл тихо випаде з процесу об'єднання без жодних індикацій у результаті. Кінцевий 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, переносять свій видимий вміст без змін. Текст, векторна графіка, растрові зображення, вбудовані шрифти та форми XObjects передаються як частина потоків вмісту сторінки. Анотації на рівні сторінки, включаючи коментарі, виділення та рукописні штрихи, також переносяться, оскільки вони зберігаються у словнику сторінки, а не на рівні документа

Метадані на рівні документа - це інша справа. Рядки заголовка, автора, теми та ключових слів у словнику Info вихідного файлу залишаються позаду. Цільовий документ починається з порожніх метаданих після CreateDocument, тому якщо в об'єднаному результаті потрібно заповнити ці поля, вам доведеться призначити їх для PdfDest безпосередньо перед викликом SaveAs. Властивості Title, Author, Subject, Keywords та Creator у TPdf приймають звичайні рядки та записують їх у словник Info під час збереження

З інтерактивними полями форм усе складніше. Визначення полів AcroForm знаходяться у словнику на рівні документа, а не всередині потоків окремих сторінок. Коли ImportPages копіює сторінку, яка містить поля форм, їхній візуальний вигляд переноситься, оскільки він рендериться у потік вмісту сторінки, але віджети полів, які роблять їх інтерактивними, є частиною структури AcroForm і не копіюються. Під час типового об'єднання текстове поле з вихідного документа відображатиме значення, яке воно мало на момент імпорту, але його не можна буде редагувати в об'єднаному файлі. Якщо вам потрібно, щоб поля зберегли свої заповнені значення, виконайте їх зведення (flatten) у кожному вихідному документі перед імпортом: це вбудовує поточні значення в потік вмісту та видаляє інтерактивний шар, даючи чистий візуальний результат без неробочих віджетів на виході

Зашифровані вихідні файли

Вихідні документи, захищені паролем, відкриваються так само, як і незашифровані, але спочатку потрібно встановити одну додаткову властивість. Призначте пароль для 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 Component для Delphi та C++Builder