Műszaki cikk

PDF dokumentumok felosztása PDFium Component-lel Delphiben

A PDFium Component egyetlen módszert (method) ad a PDF felosztására: ImportPages. Minden más – akár egyetlen oldalt különít el, akár tetszőleges határokon vág, vagy a dokumentum saját könyvjelző-struktúráját követi – csupán különböző módja annak eldöntésére, hogy mely oldalszámok kerüljenek az egyes kimeneti fájlokba. A mechanika ugyanaz marad. Ennek korai megértése sok tévutat megspórol

Hogyan működik a felosztási hurok (split loop)

A minta ugyanaz, függetlenül attól, hogyan osztja fel a forrásdokumentumot (source document). Hozzon létre egy friss TPdf példányt, hívja meg rajta a CreateDocument-et egy üres PDF memóriában történő inicializálásához, importálja a kívánt oldalakat az ImportPages segítségével, mentse az eredményt, majd állítsa vissza az Active-ot False-ra a következő iteráció (iteration) előtt. Ez az utolsó lépés az, amit az emberek gyakran kihagynak: a CreateDocument mindig új dokumentumot indít, de ha az Active még mindig True, amikor újra lefut, hallgatólagosan (implicitly) eldobja a még memóriában lévő dokumentumot, így az előzetes visszaállítás tisztán és jól definiáltan tartja az állapotot. A külső TPdf példány minden iteráció során újrahasznosításra kerül, ami alacsonyan tartja a lefoglalási nyomást (allocation pressure) nagy feladatoknál

Íme, hogyan néz ki az oldalankénti (page-by-page) felosztás a lényegre csupaszítva:

procedure SplitIntoPages(Source: TPdf; const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 1 to Source.PageCount do
    begin
      PdfOut.CreateDocument;

      // Range is a 1-based page number string; insertion point 1 = first position
      PdfOut.ImportPages(Source, IntToStr(I), 1);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      PdfOut.SaveAs(OutFile);

      PdfOut.Active := False;   // reset before next CreateDocument
    end;
  finally
    PdfOut.Free;
  end;
end;

A Range paraméter az ImportPages-hez ugyanaz a karakterlánc formátum, amelyet a PDFium belsőleg használ: oldalszámok vagy kötőjellel elválasztott tartományok vesszővel elválasztott listája, mind 1-alapú. A '3' a 3. oldalt importálja. Az '1-5' sorrendben importálja az 1-től 5-ig terjedő oldalakat. A '2,5,8' ezt a három oldalt importálja. A harmadik paraméter az 1-alapú beillesztési pozíció (insertion position) a céldokumentumban; az 1 átadása az importált oldalakat mindig egy egyébként üres fájl elejére helyezi, és ön pontosan ezt akarja itt

Felosztás oldaltartományok (page ranges) szerint

Ha a hívó egy olyan listát ad meg, mint az 1-12,13-24,25-36, ön kezdő/vég párokra bontja (parse), és ugyanazt a hurkot futtatja, minden párból felépítve a tartomány (range) karakterláncot:

procedure SplitByRanges(Source: TPdf; const RangeList: array of string;
  const OutputDir: string);
var
  I: Integer;
  PdfOut: TPdf;
  OutFile: string;
begin
  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(RangeList) do
    begin
      PdfOut.CreateDocument;
      PdfOut.ImportPages(Source, RangeList[I], 1);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      PdfOut.SaveAs(OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

A validáció az ImportPages elérése előtt itt számít. Az ImportPages False értéket ad vissza, ha a tartomány karakterláncban lévő oldalszám meghaladja a Source.PageCount értékét, de nem vet fel kivételt, és nem állít elő olyan részleges kimeneti fájlt sem, amelyet pusztán név alapján észlelni tudna. Ellenőrizze a SaveAs visszatérési értékét, és naplózza (log) a hibákat külön; egy olyan tartomány, amely üres kimeneti fájlt eredményez, nem nyilvánvalóan hibás, amíg valaki meg nem nyitja

Felosztás könyvjelző-határokon (bookmark boundaries)

A harmadik megközelítés a dokumentum saját struktúráját használja egy kívülről (externally) megadott lista helyett. Minden legfelső szintű (top-level) könyvjelző hordoz egy cél oldalszámot; az általa meghatározott szakasz (section) ettől az oldaltól a következő könyvjelző oldala előtti oldalig, vagy az utolsó bejegyzés esetén a dokumentum végéig tart

procedure SplitByBookmarks(Source: TPdf; const OutputDir: string);
var
  Bm: TBookmarks;
  I, StartPage, EndPage: Integer;
  PdfOut: TPdf;
  RangeStr, OutFile, SafeTitle: string;
begin
  Bm := Source.Bookmarks;
  if Length(Bm) = 0 then
    Exit;

  PdfOut := TPdf.Create(nil);
  try
    for I := 0 to High(Bm) do
    begin
      StartPage := Bm[I].PageNumber;
      if I < High(Bm) then
        EndPage := Bm[I + 1].PageNumber - 1
      else
        EndPage := Source.PageCount;

      if (StartPage < 1) or (EndPage < StartPage) then
        Continue;

      RangeStr := Format('%d-%d', [StartPage, EndPage]);

      PdfOut.CreateDocument;
      PdfOut.ImportPages(Source, RangeStr, 1);

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      PdfOut.SaveAs(OutFile);

      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Egy könyvjelzők nélküli dokumentum nem olyan hibaállapot (error condition), amelyet érdemes lenne a felhasználó felé akként felszínre hozni (surfacing); ez csak azt jelenti, hogy ennek a felosztási módnak nincs miből dolgoznia. A Length(Bm) = 0 őr (guard) ezt csendben kezeli. Amit viszont érdemes felszínre hozni, az az, amikor egy könyvjelző oldalszáma kívül esik a dokumentum tartományán; ez olyan rosszul formázott (malformed) fájloknál fordul elő, ahol a vázlatot (outline) soha nem frissítették az oldalak törlése után. A határellenőrzés (bounds check) a StartPage és az EndPage értékeken átugorja (skips) ezeket a bejegyzéseket ahelyett, hogy szemét (garbage) tartományt adna át az ImportPages-nek

Kimeneti fájl elnevezése és az Active visszaállítása

A könyvjelzőkből származtatott nevek (bookmark-derived names) fájlnév-biztonsága kifejezett (explicit) figyelmet igényel. A könyvjelzők címei tartalmazhatnak olyan karaktereket, amelyek érvényesek egy PDF karakterláncban, de nem érvényesek egy fájlrendszer-útvonalon (filesystem path). Legalább a perjelet, a visszaperjelet (backslash) és a kettőspontot cserélje le a kimeneti útvonal felépítése előtt. Windowson a *, ?, ", <, > és | szintén tilos; egy egyszerű hurok egy rögzített halmazon lefedi őket anélkül, hogy reguláris kifejezést (regex) kellene bevonnia

Az Active := False sor minden iteráció végén hangsúlyt érdemel, mert ez az egyetlen nem nyilvánvaló követelmény a mintában. A CreateDocument nem zárja be hallgatólagosan azt, ami éppen nyitva van. Ha az Active még mindig True, amikor a CreateDocument újra lefut, a PDFium hiba nélkül eldobja az aktuális dokumentumot, és újat indít, de a viselkedés peremesetekben (edge cases) implementáció-függő (implementation-defined), és a szándék világosabb, ha explicit módon állítja vissza. Gondoljon rá úgy, mint a try/finally párjára: a finally blokk felszabadítja a külső objektumot; az Active := False visszaállítja a belső dokumentum állapotát a hurok iterációi között

A memóriahasználat egy nagy felosztási feladat (split job) során lapos (flat) marad ezzel a megközelítéssel, mert ön soha nem tart egynél több kimeneti dokumentumot a memóriában egyszerre. A forrásdokumentum végig nyitva és csak olvasható marad; az ImportPages oldaladatokat másol az új dokumentumba a forrás módosítása nélkül. Ha a forrás titkosított, nyissa meg a jelszavával a hurok előtt, és az egyes kimeneti fájlokba másolt oldalak titkosítatlanok (unencrypted) lesznek, ami általában a helyes viselkedés a különböző címzetteknek szétosztott felosztott kimenetnél

Még egy dolog a SaveAs-ről: egy Boolean (logikai) értéket ad vissza. Egy nem létező kimeneti könyvtár, egy olyan útvonal, amelynek karaktereit az operációs rendszer elutasítja, vagy egy betelt lemez állapota mind azt eredményezi, hogy a SaveAs False értéket ad vissza kivétel felvetése (raising an exception) nélkül. Egy kötegelt feladatban (batch job), amely egy 200 oldalas dokumentumot 200 egyoldalas fájlra oszt fel, egy csendes hiba (silent failure) a 147. oldalon könnyen figyelmen kívül hagyható. Ellenőrizze a visszatérési értéket minden hívásnál, és számolja össze a sikereket a várt végösszeghez (expected total) képest, amikor a hurok befejeződik

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

A frissített felosztási útmutató a split loop működését, az oldaltartományok és könyvjelző-határok szerinti vágást, valamint az Active visszaállítását rendezi egységbe