Műszaki cikk

Több PDF fájl egyesítése egyetlen dokumentummá a PDFium Component segítségével

A PDFium Component a PDF egyesítést egyetlen metóduson, az ImportPages-en keresztül teszi elérhetővé (exposes). A minta mindig ugyanaz: hozzon létre egy üres céldokumentumot (destination document), nyissa meg az összes forrásfájlt, hívja meg az ImportPages metódust az oldalak átmásolásához (copy across), zárja be a forrást, és ismételje meg (repeat). Amikor a ciklus (loop) befejeződik, a SaveAs leírja (writes) az eredményt a lemezre. Nincs különleges egyesítési mód (merge mode), nincs átbillentendő konfiguráció (configuration to flip). A bonyolultság (complexity) a peremesetekben (edge cases) rejlik, és van néhány, ami figyelmeztetés nélkül harap

Az alapciklus

Két TPdf példány (instances) minden, amire szüksége van. Az egyik tartalmazza a céldokumentumot, amelyet üresen hoztak létre a CreateDocument segítségével. A másik sorban (in turn) megnyitja az egyes forrásfájlokat. Az alábbiakban egy eljárás (procedure) található, amely átvesz egy listát a fájlútvonalakról (file paths), és az egyesített kimenetet (merged output) egyetlen útvonalra írja:

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;

Két dolgot a kódban könnyű figyelmen kívül hagyni (overlook) első olvasásra. Az első az, hogy a PDFium hogyan jelenti a betöltési hibákat (load failures). Az Active := True soha nem dob kivételt (raises an exception): ha a fájl hiányzik, sérült vagy jelszóval védett, a PDFium belsőleg elkapja a hibát, és az Active-ot False értéken hagyja. A 10. sorban lévő explicit ellenőrzés nélkül egy rossz fájl csendben kiesne az egyesítésből anélkül, hogy ennek bármi jele (indication) lenne a kimeneten. A végső PDF-nek a vártnál kevesebb oldala lenne, és nem tudná, melyik fájl volt a bűnös (culprit)

A második az InsertAt számláló (counter). Az ImportPages harmadik argumentuma (argument) az az 1-alapú (1-based) pozíció a célban (destination), ahol az első importált oldal landol. Az 1-nél történő kezdés (starting at 1) az első forrásdokumentumot egy egyébként üres fájl elejére helyezi. Minden forrás után a számláló előrelép a PdfSrc.PageCount értékével, így a következő adag (batch) oldal az utolsó után fűződik hozzá (appends). Ha elfelejti növelni (increment), akkor minden további forrás felülírja (overwrites) az 1-es pozícióban lévő oldalakat, így a lista utolsó dokumentumát kapja, és semmi mást

Szelektív oldaltartományok

Nem kell a forrásból minden oldalt elvennie (take). A második argumentumként átadott tartomány-sztring (range string) egy egyszerű vessző-kötőjel (comma-and-hyphen) formátumot követ: az "1-3" az 1-től 3-ig tartó oldalakat veszi, a "2,4,6" három konkrét (specific) oldalt választ ki, az "1-" pedig az 1. oldaltól a dokumentum végéig tartó részt jelenti. A tartományok (ranges) kombinálhatók egyetlen sztringben, így az "1-3,5,7-" kihagyja a 4. és 6. oldalt. Egy finomság (subtlety) számít itt: a számok mindig a forrásdokumentum oldalaira vonatkoznak, 1-től kezdve, függetlenül attól, hogy ezek az oldalak hol kötnek ki a célban (end up in the destination). Ha egy 200 oldalas katalógusból a 40-50. oldalakat szeretné, a tartomány-sztring "40-50", nem pedig a célban már meglévőhöz viszonyított relatív pozíció

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

Az InsertAt növekményének (increment) kiszámításakor azokat az oldalakat számolja, amelyeket ténylegesen importált, ne pedig a forrás oldalszámát (page count). Ha a '1,3-5' értéket adja át, 4 oldalt importált, tehát 4-gyel lépjen előre. A PdfSrc.PageCount értékkel való előrelépés üres célpozíciók (blank destination positions) hézagát hagyná maga után, és a következő forrásdokumentumot a szándékoltnál távolabb (further into the file) helyezné el a fájlban

Mit őriz meg az ImportPages és mit nem

Az ImportPages által másolt oldalak a látható tartalmukat (visible content) érintetlenül (intact) hordozzák. A szöveg (text), vektorgrafikák (vector graphics), raszterképek (raster images), beágyazott betűtípusok (embedded fonts) és űrlap XObjectek (form XObjects) mind átkerülnek (transfer) az oldaltartalom-adatfolyamok (page content streams) részeként. Az oldalszintű (page-level) megjegyzések (annotations), beleértve a kommenteket, kiemeléseket (highlights) és tintavonásokat (ink strokes), szintén átjönnek (come across), mivel azok az oldalszótárban (page dictionary) vannak tárolva, nem pedig dokumentumszinten (document level)

A dokumentumszintű metaadatok (document-level metadata) más lapra tartoznak (different story). A forrás (source) Információs szótárában (Info dictionary) lévő cím (title), szerző (author), tárgy (subject) és kulcsszó (keyword) sztringek hátramaradnak. A céldokumentum (destination document) üres metaadatokkal indul a CreateDocument után, így ha az egyesített kimenetnek (merged output) ezekre a mezőkre (fields) szüksége van feltöltve (populated), akkor azokat közvetlenül a PdfDest-hez kell hozzárendelnie (assign) a SaveAs meghívása előtt. A TPdf Title, Author, Subject, Keywords és Creator tulajdonságai egyszerű sztringeket (plain strings) fogadnak el, és mentéskor az Info szótárba írnak

Az interaktív űrlapmezők (interactive form fields) bonyolultabbak. Az AcroForm meződefiníciók (field definitions) egy dokumentumszintű szótárban élnek, nem pedig az egyes oldal-adatfolyamokban (individual page streams). Amikor az ImportPages átmásol egy olyan oldalt, amely űrlapmezőket tartalmaz, ezen mezők vizuális megjelenése (visual appearance) átkerül (transfers), mivel bele van renderelve az oldaltartalom-adatfolyamba (page content stream), de az azokat interaktívvé tévő mező-widgetek (field widgets) az AcroForm struktúra részét képezik, és nem követik őket (do not follow). Egy tipikus egyesítés során a forrásdokumentumból származó szövegmező (text field) az importálás időpontjában meglévő értéket jeleníti meg, de az egyesített fájlban nem lesz szerkeszthető (editable). Ha arra van szüksége, hogy a mezők kitölthetők maradjanak (remain fillable), lapítsa ki (flatten) őket minden forrásdokumentumban az importálás előtt: ez belesüti (bakes) az aktuális értékeket a tartalom-adatfolyamba, és eltávolítja az interaktív rátétet (interactive overlay), tiszta vizuális eredményt (clean visual result) adva törött widgetek nélkül a kimeneten

Titkosított forrásfájlok

A jelszóval védett (password-protected) forrásdokumentumok ugyanúgy nyílnak meg, mint a titkosítatlanok (unencrypted ones), először egy extra tulajdonságot (property) kell beállítani. Rendelje hozzá a jelszót a PdfSrc.Password-höz az Active := True átbillentése előtt, és a PDFium használni fogja azt a megnyitás során (during the open):

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;

Egy rossz jelszó (wrong password) ugyanolyan csendes (silent) Active = False kimenetelt (outcome) eredményez, mint egy hiányzó fájl, így az explicit ellenőrzés itt is ugyanolyan szükséges. A titkosítás nem kerül át a célba (destination): a védett forrásból importált oldalak védetlen tartalomként (unprotected content) landolnak a célban. Ha az egyesített kimenetnek (merged output) is szüksége van titkosításra, konfigurálja azt a PdfDest-en a SaveAs meghívása előtt

Az eredmény mentése

A TPdf SaveAs metódusa vagy egy fájlútvonalat, vagy egy TStream-et fogad el. A legtöbb egyesítésnél (merges) a fájl túlterhelés (file overload) az, amire önnek szüksége van:

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

Az opcionális második argumentum egy TSaveOption, amely a mentési módot vezérli. Az alapértelmezett (default), a saNone egy inkrementális frissítést (incremental update) ír, ha a dokumentumot egy fájlból töltötték be, vagy egy teljes újraírást (complete rewrite), ha frissen hozták létre. Mivel a CreateDocument-tel felépített cél (destination) mindig friss (fresh), a kimenet egy kompakt, egyetlen revíziójú (single-revision) fájl lesz. A harmadik argumentum, a TPdfVersion lehetővé teszi, hogy kitűzze (pin) a PDF verzió fejlécét (version header), ha olyan downstream fogyasztókkal (downstream consumers) rendelkezik, amelyek egy adott verziót igényelnek; ha a pvUnknown-on hagyja, a PDFium a tartalom alapján fog választani

Az itt bemutatott ImportPages és SaveAs metódusok a Delphihez és C++Builderhez készült PDFium Component részét képezik