Technical Article

Обединяване на множество 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. Без изричната проверка, повреденият файл просто ще бъде изпуснат от обединяването без никаква индикация в изходния резултат. Крайният 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 копира страница, съдържаща полета на формуляр, визуалният вид на тези полета се прехвърля, тъй като се изобразява в потока от съдържание на страницата, но графичните компоненти (widgets), които ги правят интерактивни, са част от 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 VCL Component за Delphi и C++Builder.