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
Í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
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