Műszaki cikk

PDF-dokumentumok szétvágása PDFium Componenttel Delphiben

A PDFium Component egyetlen metódust ad a PDF szétvágásához: az ImportPages hívást. Minden más – akár egyetlen oldalt különítünk el, akár tetszőleges határok mentén vágunk, akár a dokumentum saját könyvjelzőszerkezetét követjük – csupán más-más módja annak, hogy eldöntsük, melyik oldalszámok kerüljenek az egyes kimeneti fájlokba. A mechanika ugyanaz marad. Ha ezt korán megértjük, sok tévutat spórolunk meg

Hogyan működik a szétvágó ciklus

A minta ugyanaz, függetlenül attól, hogyan osztjuk fel a forrásdokumentumot. Hozzunk létre új TPdf példányt, hívjuk meg rajta a CreateDocument metódust, hogy üres PDF-et inicializáljon a memóriában, importáljuk a kívánt oldalakat az ImportPages hívással, mentsük el az eredményt, majd a következő iteráció előtt állítsuk az Active tulajdonságot False értékre. Ezt az utolsó lépést szokták kihagyni: a CreateDocument nem zárja be magától a memóriában még bent lévő dokumentumot, ezért mentenünk kell a kimenetet, és kifejezetten vissza kell állítanunk az Active := False értéket, mielőtt újra meghívnánk; az előzetes visszaállítás tisztán és jól meghatározottan tartja az állapotot. A külső TPdf példány minden iteráción át újrahasznosul, ami nagy munkáknál alacsonyan tartja a foglalási terhelést

A PDFium Component szétvágó ciklusának ábrája Delphiben: CreateDocument, ImportPages a csak olvasható forrásból, ellenőrzött SaveAs, majd az Active visszaállítása minden új iteráció előtt
Bármi dönti is el a csoportokat, a ciklus azonos marad: importáljuk az oldalakat, mentsünk ellenőrzött eredménnyel, majd állítsuk vissza az Active értéket, hogy a következő CreateDocument tiszta állapotból induljon

Így néz ki az oldalankénti szétvágás a lényegére 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;

      // A Range 1-alapú oldalszám-karakterlánc; az 1-es beszúrási pont az első helyet jelenti
      if not PdfOut.ImportPages(Source, IntToStr(I), 1) then
        raise Exception.CreateFmt('Failed to import page %d', [I]);

      OutFile := OutputDir + '\page_' + Format('%.4d', [I]) + '.pdf';
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

      PdfOut.Active := False;   // visszaállítás a következő CreateDocument előtt
    end;
  finally
    PdfOut.Free;
  end;
end;

Az ImportPages Range paramétere ugyanaz a karakterlánc-formátum, amelyet a PDFium belül használ: oldalszámok vesszővel elválasztott listája vagy kötőjellel határolt tartományok, mind 1-alapúan. A '3' a 3. oldalt importálja. Az '1-5' az 1–5. oldalt importálja sorrendben. A '2,5,8' ezt a három oldalt importálja. A harmadik paraméter az 1-alapú beszúrási pozíció a céldokumentumban; az 1 érték átadása az importált oldalakat mindig egy egyébként üres fájl elejére helyezi, és itt éppen ezt akarjuk

Szétvágás oldaltartományok szerint

Amikor a hívó egy 1-12,13-24,25-36 alakú listát ad meg, ezt kezdő-záró párokra bontjuk, és ugyanazt a ciklust futtatjuk, minden párból felépítve a tartomány-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;
      if not PdfOut.ImportPages(Source, RangeList[I], 1) then
        raise Exception.Create('Invalid page range: ' + RangeList[I]);
      OutFile := Format('%s\section_%d.pdf', [OutputDir, I + 1]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);
      PdfOut.Active := False;
    end;
  finally
    PdfOut.Free;
  end;
end;

Az ImportPages hívás előtti ellenőrzés itt számít. Az ImportPages False értéket ad vissza, ha a tartomány-karakterlánc egyik oldalszáma meghaladja a Source.PageCount értéket, de nem vált ki kivételt, és nem hoz létre olyan részleges kimeneti fájlt, amelyet pusztán a nevéből felismernénk. Ellenőrizzük a SaveAs visszatérési értékét, és a hibákat naplózzuk külön; az a tartomány, amely üres kimeneti fájlt eredményez, addig nem látszik hibásnak, amíg valaki meg nem nyitja

Szétvágás könyvjelzőhatárok mentén

A harmadik megközelítés a dokumentum saját szerkezetét használja külső lista helyett. Minden felső szintű könyvjelző hordoz egy céloldalszámot; az általa meghatározott szakasz ettől az oldaltól a következő könyvjelző oldala előtti oldalig tart, az utolsó bejegyzésnél pedig a dokumentum végéig

Ábra arról, hogyan képződnek le a felső szintű PDF-könyvjelzők számított oldaltartományokra és kimeneti fájlokra a PDFium Component Delphi szétvágásánál, egy tartományon kívüli, kihagyott könyvjelzővel együtt
Egy szakasz minden felső szintű könyvjelző oldalától a következő könyvjelző előtti oldalig tart, a végen túlra mutató bejegyzések pedig kimaradnak, ahelyett hogy üres fájlokat gyártanának
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;
      if not PdfOut.ImportPages(Source, RangeStr, 1) then
      begin
        PdfOut.Active := False;
        Continue;   // hibás szakasz kihagyása üres fájl írása helyett
      end;

      SafeTitle := StringReplace(Bm[I].Title, '/', '_', [rfReplaceAll]);
      SafeTitle := StringReplace(SafeTitle, ':', '_', [rfReplaceAll]);
      OutFile := Format('%s\%02d_%s.pdf', [OutputDir, I + 1, SafeTitle]);
      if not PdfOut.SaveAs(OutFile) then
        raise Exception.Create('Failed to save ' + OutFile);

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

Az a dokumentum, amelyben nincsenek könyvjelzők, nem olyan hibaállapot, amelyet ilyenként a felhasználó elé kellene tárni; egyszerűen azt jelenti, hogy ennek a szétvágási módnak nincs mihez nyúlnia. A Length(Bm) = 0 ellenőrzés ezt némán kezeli. Amit viszont érdemes felszínre hozni, az az, ha egy könyvjelző oldalszáma a dokumentum tartományán kívül esik; ez olyan hibás fájloknál fordul elő, ahol a vázlatot soha nem frissítették oldalak törlése után. A StartPage és EndPage határellenőrzése kihagyja ezeket a bejegyzéseket ahelyett, hogy hulladéktartományt adna át az ImportPages hívásnak

A kimeneti fájlok elnevezése és az Active visszaállítása

A könyvjelzőkből származó nevek fájlnévbiztonsága kifejezett figyelmet kíván. A könyvjelzőcímek olyan karaktereket tartalmazhatnak, amelyek egy PDF-karakterláncban érvényesek, egy fájlrendszer-útvonalban viszont nem. Legalább a perjelet, a fordított perjelet és a kettőspontot cseréljük le a kimeneti útvonal felépítése előtt. Windowson a *, a ?, a ", a <, a > és a | szintén tiltott; egy rögzített halmazon futó egyszerű ciklus lefedi őket anélkül, hogy reguláris kifejezést kellene behúzni

A minden iteráció végén álló Active := False sor hangsúlyt érdemel, mert ez a minta egyetlen nem kézenfekvő követelménye. A CreateDocument nem zárja be magától azt, ami éppen nyitva van. Ha az Active még True, amikor a CreateDocument újra lefut, a memóriában lévő dokumentumot soha nem zártuk le és nem mentettük el rendesen, és ebben az állapotban nem támaszkodhatunk jól meghatározott viselkedésre, ezért a következő dokumentum indítása előtt kifejezetten mentsünk és állítsuk vissza. Gondoljunk rá úgy, mint a try/finally párjára: a finally blokk a külső objektumot szabadítja fel, az Active := False pedig a belső dokumentum állapotát állítja vissza a ciklus iterációi között

Egy nagy szétvágási munka memóriahasználata ezzel a megközelítéssel egyenletes marad, mert soha nem tartunk egyszerre egynél több kimeneti dokumentumot a memóriában. A forrásdokumentum végig nyitva és csak olvashatóan marad; az ImportPages az oldaladatokat az új dokumentumba másolja anélkül, hogy a forrást módosítaná. Ha a forrás titkosított, a ciklus előtt nyissuk meg a jelszavával, és az egyes kimeneti fájlokba másolt oldalak titkosítatlanok lesznek, ami különböző címzetteknek szétosztott szétvágott kimenetnél rendszerint a helyes viselkedés

Még valami a SaveAs hívásról: Boolean értéket ad vissza. Egy nem létező kimeneti könyvtár, egy olyan karaktereket tartalmazó útvonal, amelyet az operációs rendszer elutasít, vagy egy megtelt lemez mind azt eredményezi, hogy a SaveAs False értékkel tér vissza, kivétel kiváltása nélkül. Egy kötegelt munkában, amely egy 200 oldalas dokumentumot 200 egyoldalas fájlra vág, a 147. oldalon bekövetkező néma hibát könnyű átsiklani. Ellenőrizzük a visszatérési értéket minden hívásnál, és a ciklus végén vessük össze a sikerek számát a várt összeggel

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