Technical Article

Spájanie viacerých PDF súborov do jedného dokumentu pomocou PDFium VCL

PDFium VCL sprístupňuje spájanie PDF prostredníctvom jedinej metódy: ImportPages. Postup je vždy rovnaký: vytvoríte prázdny cieľový dokument, otvoríte každý zdrojový súbor, zavoláte ImportPages na skopírovanie stránok, zatvoríte zdrojový súbor a proces opakujete. Po skončení cyklu metóda SaveAs zapíše výsledok na disk. Neexistuje žiadny špeciálny režim spájania ani zložité nastavenia. Zložitosť sa však skrýva v okrajových prípadoch a je ich niekoľko, na ktoré môžete naraziť bez varovania.

Hlavný cyklus

Všetko, čo potrebujete, sú dve inštancie triedy TPdf. Jedna drží cieľový dokument, vytvorený ako prázdny pomocou CreateDocument. Druhá postupne otvára každý zdrojový súbor. Nižšie je uvedená procedúra, ktorá berie zoznam ciest k súborom a zapíše zlúčený výstup na jednu cieľovú cestu:

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;

Dve veci v tomto kóde je pri prvom čítaní ľahké prehliadnuť. Prvou je spôsob, akým PDFium hlási zlyhania načítania. Priradenie Active := True nikdy nevyvolá výnimku: ak súbor chýba, je poškodený alebo chránený heslom, PDFium zachytí chybu interne a ponechá vlastnosť Active na hodnote False. Bez explicitnej kontroly na riadku 10 by sa poškodený súbor ticho vynechal zo spájania bez akéhokoľvek upozornenia vo výstupe. Výsledné PDF by malo menej stránok, než sa očakávalo, a vy by ste nevedeli, ktorý súbor to spôsobil.

Druhou vecou je počítadlo InsertAt. Tretí argument metódy ImportPages je pozícia v cieľovom dokumente (indexovaná od 1), kam dopadne prvá importovaná stránka. Začiatok na hodnote 1 umiestni prvý zdrojový dokument na začiatok inak prázdneho súboru. Po každom zdroji sa počítadlo posunie o hodnotu PdfSrc.PageCount, takže ďalšia dávka stránok sa pripojí za tú predchádzajúcu. Ak na zvýšenie počítadla zabudnete, každý ďalší zdroj prepíše stránky na pozícii 1, takže vo výsledku získate iba posledný dokument zo zoznamu.

Selektívne rozsahy stránok

Zo zdroja nemusíte brať každú stránku. Reťazec rozsahu odovzdávaný ako druhý argument nasleduje jednoduchý formát s čiarkami a spojovníkmi: "1-3" vezme strany 1 až 3, "2,4,6" vyberie tri konkrétne strany a "1-" znamená stranu 1 až po koniec dokumentu. Rozsahy sa dajú kombinovať v jednom reťazci, takže "1-3,5,7-" preskočí strany 4 a 6. Dôležitý detail: čísla strán sa vždy vzťahujú na zdrojový dokument (počnúc od 1) bez ohľadu na to, kde tieto stránky skončia v cieľovom súbore. Ak chcete z 200-stránkového katalógu vybrať strany 40 až 50, reťazec rozsahu bude "40-50", nie pozícia relatívna voči tomu, čo už v cieľovom súbore je.

// 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;

Pri výpočte prírastku pre InsertAt počítajte stránky, ktoré ste skutočne importovali, nie celkový počet strán zdroja. Ak odovzdáte reťazec '1,3-5', importovali ste 4 stránky, takže počítadlo posuňte o 4. Posunutie o PdfSrc.PageCount by zanechalo medzeru s prázdnymi stranami v cieľovom dokumente a umiestnilo by ďalší zdrojový dokument ďalej, než bolo zamýšľané.

Čo metóda ImportPages zachováva a čo nie

Stránky skopírované metódou ImportPages si zachovávajú svoj viditeľný obsah nedotknutý. Text, vektorová grafika, rastrové obrázky, vložené písma a formulárové objekty XObject sa prenesú ako súčasť prúdov obsahu stránky. Prenášajú sa aj anotácie na úrovni stránky (vrátane komentárov, zvýraznení a ťahov perom), pretože sú uložené v slovníku stránky a nie na úrovni celého dokumentu.

Metadáta na úrovni dokumentu sú iný prípad. Názov, autor, predmet a kľúčové slová v slovníku Info zdrojového súboru zostávajú pozadu. Cieľový dokument začína po volaní CreateDocument s prázdnymi metadátami, takže ak ich potrebujete vyplniť, musíte ich objektu PdfDest priradiť priamo pred volaním SaveAs. Vlastnosti Title, Author, Subject, Keywords a Creator v triede TPdf prijímajú bežné reťazce a zapisujú ich do slovníka Info pri ukladaní.

Interaktívne formulárové polia sú zložitejšie. Definície polí AcroForm sa nachádzajú v slovníku na úrovni dokumentu, nie v prúdoch jednotlivých stránok. Keď metóda ImportPages kopíruje stránku, ktorá obsahuje formulárové polia, ich vizuálny vzhľad sa prenesie, pretože je vykreslený priamo v prúde obsahu stránky. Avšak ovládacie prvky (widgety), ktoré ich robia interaktívnymi, sú súčasťou štruktúry AcroForm a neprenášajú sa. Pri bežnom zlúčení bude textové pole zo zdrojového dokumentu zobrazovať hodnotu, ktorú malo v čase importu, ale v zlúčenom súbore už nebude upraviteľné. Ak potrebujete, aby polia zostali vyplniteľné, vykonajte ich zlúčenie do obsahu (flattening) v každom zdrojovom dokumente pred importom: tým sa aktuálne hodnoty zapečú do prúdu obsahu a odstráni sa interaktívna vrstva, čo vám poskytne čistý vizuálny výsledok bez nefunkčných prvkov vo výstupe.

Šifrované zdrojové súbory

Heslom chránené zdrojové dokumenty sa otvárajú rovnakým spôsobom ako nezašifrované, s jednou vlastnosťou navyše, ktorú treba nastaviť ako prvú. Priraďte heslo vlastnosti PdfSrc.Password pred nastavením Active := True a PDFium ho použije pri otváraní:

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;
```

Nesprávne heslo spôsobuje rovnaké tiché zlyhanie (Active = False) ako chýbajúci súbor, takže explicitná kontrola je tu rovnako dôležitá. Šifrovanie sa do cieľového dokumentu neprenáša: stránky importované z chráneného zdroja pristanú v cieli ako nechránený obsah. Ak vyžadujete, aby bol aj zlúčený výstup šifrovaný, nakonfigurujte šifrovanie na objekte PdfDest pred volaním SaveAs.

Uloženie výsledku

Metóda SaveAs v triede TPdf prijíma buď cestu k súboru, alebo objekt TStream. Pre väčšinu zlučovaní je správnou voľbou preťaženie so súborom:

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

Voliteľný druhý argument je príznak TSaveOption, ktorý riadi režim ukladania. Predvolená hodnota saNone zapíše prírastkovú aktualizáciu, ak bol dokument načítaný zo súboru, alebo vykoná kompletný prepis, ak bol vytvorený nanovo. Keďže cieľový súbor vytvorený cez CreateDocument je vždy nový, výstupom bude kompaktný súbor s jednou revíziou. Tretí argument, TPdfVersion, vám umožňuje vynútiť hlavičku verzie PDF, ak nadväzujúce aplikácie vyžadujú konkrétnu verziu. Ak ho ponecháte na pvUnknown, PDFium zvolí verziu automaticky na základe obsahu.

Metódy ImportPages a SaveAs popísané v tomto článku sú súčasťou komponentu PDFium VCL Component pre Delphi a C++Builder.