Technický článek

Sloučení více souborů PDF do jednoho dokumentu pomocí komponenty PDFium

Komponenta PDFium zpřístupňuje slučování PDF prostřednictvím jediné metody: ImportPages. Postup je vždy stejný: vytvoříte prázdný cílový dokument, otevřete každý zdrojový soubor, zavoláte ImportPages pro zkopírování stránek, zavřete zdroj a opakujete. Jakmile smyčka skončí, metoda SaveAs zapíše výsledek na disk. Neexistuje žádný speciální režim slučování, žádná konfigurace, kterou by bylo nutné přepínat. Složitost tkví v okrajových případech a je jich pár, které kousnou bez varování

Základní smyčka

Dvě instance TPdf jsou vše, co potřebujete. Jedna obsahuje cílový dokument, vytvořený jako prázdný pomocí CreateDocument. Druhá postupně otevírá každý zdrojový soubor. Níže je procedura, která přijme seznam cest k souborům a zapíše sloučený výstup do jediné cesty:

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;

Ve výše uvedeném kódu lze při prvním čtení snadno přehlédnout dvě věci. První z nich je to, jak PDFium oznamuje selhání při načítání. Příkaz Active := True nikdy nevyvolá výjimku: pokud soubor chybí, je poškozený nebo chráněný heslem, PDFium chybu interně odchytí a ponechá Active nastavené na False. Bez explicitní kontroly na řádku 10 by chybný soubor ze slučování tiše vypadl bez jakéhokoliv náznaku ve výstupu. Výsledné PDF by pak mělo méně stránek, než jste očekávali, a vy byste neměli tušení, který soubor to způsobil

Druhou věcí je počítadlo InsertAt. Třetím argumentem funkce ImportPages je pozice v cílovém dokumentu indexovaná od jedničky, kam dopadne první importovaná stránka. Začátek na 1 vloží první zdrojový dokument na začátek jinak prázdného souboru. Po každém zdroji se pak počítadlo posune o PdfSrc.PageCount, takže další dávka stránek se připojí až za tu poslední. Pokud jej zapomenete navýšit, každý následující zdroj přepíše stránky na pozici 1 a získáte tak poslední dokument ze seznamu a nic jiného

Výběrové rozsahy stran

Nemusíte si ze zdroje brát hned každou stránku. Řetězec pro určení rozsahu, předaný jakožto druhý argument, má jednoduchý formát na bázi čárek a spojovníků: příkaz "1-3" vezme strany 1 až 3, ten ve znění "2,4,6" si naopak vybere rovnou tři specifické stránky, přičemž "1-" bude znamenat strany od první až do konce samotného dokumentu. Jednotlivé intervaly se dají naprosto hladce kombinovat v jednom společném řetězci, tím pádem kupříkladu tvar "1-3,5,7-" zcela vynechá stánky číslo 4 a 6. Zde ovšem hraje důležitou roli jedna zásadní jemnost: onaká čísla totiž vždycky a bez výjimky odkazují přímo na stránky v původním zdrojovém dokumentu (počítáno od jedničky), a to naprosto bez ohledu na to, kde pak nakonec skončí ve výsledném cíli. Kdybyste tak z dvousetstránkového katalogu toužili jenom po těch stranách od čísla 40 až do 50, daný řetězec rozsahu pro to zní "40-50", nikoliv pak hodnota jakkoliv závislá na tom, co už je zapsané uvnitř vaší destinace

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

Při počítání přírůstku pro InsertAt počítejte s počtem stránek, které jste ve skutečnosti importovali, nikoliv s počtem stránek zdroje. Pokud předáte '1,3-5', naimportovali jste 4 stránky, takže se posuňte o 4. Posunutí o PdfSrc.PageCount by totiž na cílových pozicích zanechalo mezeru tvořenou prázdnými stranami a další zdrojový dokument by se tak do souboru umístil mnohem dál, než jste původně zamýšleli

Co ImportPages zachovává a co naopak nikoliv

Stránky zkopírované pomocí funkce ImportPages si s sebou přenášejí svůj viditelný obsah v naprosto neporušeném stavu. Text, vektorová grafika, rastrové obrázky, vložená písma i objekty formátu XObjects pro formuláře, to všechno se společně s nimi přesouvá v rámci obsahových proudů jednotlivých stran. Přenesou se navíc i případné anotace zasazené na úroveň stránek, a to včetně komentářů, zvýrazněného textu a inkoustových tahů, protože jsou uložené spíše uvnitř slovníku stránky (page dictionary) nežli na té nejvyšší, tedy dokumentové úrovni

U metadat uložených na úrovni dokumentu to ale bývá poněkud odlišný příběh. Řetězce z informačního slovníku zdrojového dokumentu – jako je třeba název, autor, předmět a klíčová slova – při slučování zůstávají tam, kde jsou. Po provedení CreateDocument startuje onen cílový dokument vždy jenom s prázdnými metadaty. Je-li tudíž nutné, aby měl sloučený výstup zmíněná políčka opět hezky vyplněná, budete je muset před zavoláním volby SaveAs přiřadit rovnou k objektu PdfDest. Nastavení položek z TPdf nesoucích pojmenování jako Title (Název), Author (Autor), Subject (Předmět), Keywords (Klíčová slova) a Creator (Tvůrce) přijímá zcela obyčejné textové řetězce a v samotný moment uložení je posléze pečlivě zanese zpátky do příslušného slovníku Informací (Info dictionary)

Interaktivní pole formulářů jsou ještě o poznání složitější. Definice polí v rámci formátu AcroForm sídlí ve slovníku na úrovni dokumentu, a nikoliv přímo uvnitř proudů jednotlivých stran. Když se tak skrze metodu ImportPages zkopíruje stránka plná takových formulářových políček, jejich vizuální podoba sice poputuje dál, jelikož je naplno vykreslena do samotného toku obsahu stránky. Nicméně widgety samotných políček, které jim dodávají onu interaktivitu, představují nedílnou součást struktury AcroForm, a tím pádem v konečném součtu zkrátka za zbytkem nenásledují. V rámci toho naprosto nejtypičtějšího sloučení se to má následovně: ono pole s textem převzaté z primárního zdrojového dokumentu sice bez problému ukáže svou původní, předem naplněnou hodnotu odpovídající době jeho samotného načtení. Nikdy ho už ale naneštěstí přímo uvnitř ve spojeném souboru zkrátka zpětně nepoupravíte. Aby tedy tato zmíněná místa zůstávala i nadále bezproblémově připravená k budoucímu vpisování, ze všeho nejdříve je musíte už ve výchozích souborech takříkajíc zploštit (tzv. flatten), nežli je tam všechny posléze přenesete naostro: jen takovýmto krokem totiž zapečete momentální obsahy nekompromisně do toku informací, úplně se zbavíte onoho propojujícího interaktivního potahu, a navíc dosáhnete stoprocentně bezvadného vzhledu i bez toho, aniž by se v konečném výstupu nakupily kdejaké poškozené doplňky

Zašifrované zdrojové soubory

Zdrojové dokumenty chráněné heslem se otevírají stejným způsobem jako ty nezašifrované, pouze je třeba nejprve nastavit jednu dodatečnou vlastnost. Před překlopením proměnné Active := True přiřaďte heslo do PdfSrc.Password a PDFium ho pak bez potíží použije během jejich samotného otevírání:

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;

Špatné heslo způsobí tentýž tichý výsledek Active = False jako chybějící soubor, takže i zde je nezbytně nutná ona explicitní kontrola. Šifrování se do samotného cíle ovšem nepřesouvá: stránky importované z jakkoliv chráněného zdroje přistanou do vaší cílové destinace ve zcela nechráněné podobě. Pokud však musí i sloučený výsledný soubor obsahovat prvky šifrování, zkrátka jej nejprve patřičně nastavte v objektu PdfDest dřív, než nadobro spustíte pokyn s názvem SaveAs

Uložení výsledku

SaveAs na objektu TPdf akceptuje buď cestu k souboru, nebo datový proud TStream. Pro většinu slučování budete zřejmě chtít použít přetížení (overload) pracující rovnou se souborem:

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

Volitelným druhým argumentem je TSaveOption, který řídí režim pro ukládání. Výchozí hodnota, saNone, zapíše přírůstkovou aktualizaci, pokud byl dokument načten ze souboru, nebo provede kompletní přepis, pokud byl nově vytvořen. Protože cíl sestavený metodou CreateDocument je pokaždé jaksepatří zbrusu nový, bude z toho na konci kompaktní soubor s jednou jedinou provedenou revizí. Třetí zmíněný argument – čili onen zavedený prvek s označením TPdfVersion – se s úspěchem zasadí o připíchnutí požadovaného typu hlavičky, je-li to zapotřebí ze strany všelijakých odběratelských komponentů žádajících po tvůrci striktně stanovenou verzi onoho PDF formátu; pro případ jeho bezprizorního zanechání na hodnotě pvUnknown posléze PDFium zkrátka vykouzlí příhodnou volbu zcela a naplno závislou od právě zpracovávaného datového pole

Zde představené metody – jmenovitě ImportPages a SaveAs – platí za nedílnou součást rodiny patřící k oné PDFium Component, ušitá pro systém Delphi i C++Builder