Komponenta PDFium vám dává jednu metodu pro rozdělení PDF: ImportPages. Všechno ostatní, ať už izolujete jedinou stránku, řežete na libovolných hranicích, nebo sledujete vlastní strukturu záložek dokumentu, jsou jen různé způsoby, jak rozhodnout, která čísla stránek půjdou do každého výstupního souboru. Mechanika zůstává stejná. Pokud to pochopíte hned na začátku, ušetříte si spoustu špatných odboček
Jak funguje smyčka rozdělení
Vzor je stejný bez ohledu na to, jak zdrojový dokument rozdělíte. Vytvořte čerstvou instanci TPdf, zavolejte na ni CreateDocument, aby se v paměti inicializovalo prázdné PDF, importujte požadované stránky pomocí ImportPages, uložte výsledek a poté před další iterací resetujte Active na False. Tento poslední krok lidem často uniká: CreateDocument implicitně nezavírá dokument, který je stále v paměti, takže musíte uložit svůj výstup a explicitně resetovat Active := False, než jej zavoláte znovu; resetování jako první udržuje stav čistý a dobře definovaný. Vnější instance TPdf je znovu používána napříč všemi iteracemi, což udržuje tlak na alokaci při velkých úlohách na nízké úrovni
Zde je ukázka, jak vypadá rozdělování stránku po stránce, osekané na to nejnutnější:
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
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; // reset before next CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Parametr Range u ImportPages má stejný formát řetězce, jaký PDFium používá interně: čárkou oddělený seznam čísel stránek nebo rozsahy oddělené spojovníkem, vše číslováno od jedničky. '3' importuje stránku 3. '1-5' importuje stránky 1 až 5 v tomto pořadí. '2,5,8' importuje tyto tři stránky. Třetím parametrem je pozice vložení v cílovém dokumentu (počítáno od jedničky); předání hodnoty 1 vždy umístí importované stránky na začátek jinak prázdného souboru, což je to, co zde požadujete
Rozdělení podle rozsahů stránek
Když volající dodá seznam jako 1-12,13-24,25-36, analyzujete jej na páry start/end a spustíte stejnou smyčku, přičemž z každého páru sestavíte řetězec rozsahu:
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;
Zde záleží na validaci dříve, než se dostanete k ImportPages. ImportPages vrátí False, když číslo stránky v řetězci rozsahu překročí Source.PageCount, ale nevyvolá výjimku a nevyprodukuje částečný výstupní soubor, který byste mohli detekovat pouze podle jména. Zkontrolujte návratovou hodnotu SaveAs a selhání logujte samostatně; rozsah, který produkuje prázdný výstupní soubor, není zjevně špatný, dokud jej někdo neotevře
Rozdělení na hranicích záložek
Třetí přístup využívá vlastní strukturu dokumentu namísto externě dodaného seznamu. Každá záložka nejvyšší úrovně (top-level bookmark) nese cílové číslo stránky; sekce, kterou definuje, běží od této stránky až do stránky před další záložkou, nebo na konec dokumentu u posledního záznamu
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; // skip a malformed section instead of writing an empty file
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;
Dokument, který nemá žádné záložky, není chybovým stavem, který by stálo za to uživateli takto předkládat; znamená to jen, že tento režim rozdělování nemá s čím pracovat. Stráž Length(Bm) = 0 to zpracovává tiše. Co však stojí za to vyzdvihnout, je to, když je číslo stránky záložky mimo rozsah dokumentu, což se stává u poškozených (malformed) souborů, kde osnova nebyla nikdy aktualizována po odstranění stránek. Kontrola mezí na StartPage a EndPage přeskočí tyto záznamy, místo aby předávala nesmyslný rozsah do ImportPages
Pojmenování výstupních souborů a resetování Active
Bezpečnost názvů souborů odvozených od záložek vyžaduje výslovnou pozornost. Názvy záložek mohou obsahovat znaky, které jsou platné v řetězci PDF, ale nikoli v cestě k souborovému systému. Před sestavením výstupní cesty nahraďte minimálně lomítko, zpětné lomítko a dvojtečku. Na Windows jsou také zakázány znaky *, ?, ", <, > a |; jednoduchá smyčka přes pevnou sadu je pokryje, aniž by bylo nutné zavádět regulární výrazy (regex)
Řádek Active := False na konci každé iterace si zaslouží zdůraznění, protože je jediným nezřejmým požadavkem v tomto vzoru. CreateDocument implicitně nezavírá to, co je otevřené. Pokud je Active stále True, když se CreateDocument spustí znovu, dokument v paměti nebyl nikdy správně uzavřen nebo uložen a v tomto stavu se nelze spolehnout na dobře definované chování, takže před zahájením dalšího dokumentu výslovně uložte a resetujte. Přemýšlejte o tom jako o páru k try/finally: blok finally uvolní vnější objekt; Active := False resetuje vnitřní stav dokumentu mezi iteracemi smyčky
Využití paměti napříč velkou úlohou rozdělování zůstává s tímto přístupem konstantní, protože v paměti nikdy nedržíte více než jeden výstupní dokument najednou. Zdrojový dokument zůstává po celou dobu otevřený a pouze pro čtení; ImportPages zkopíruje data stránky do nového dokumentu beze změny zdroje. Pokud je zdroj zašifrován, otevřete jej před smyčkou s jeho heslem a zkopírované stránky v každém výstupním souboru budou nezašifrované, což je obvykle správné chování pro rozdělený výstup distribuovaný různým příjemcům
Ještě jedna věc k SaveAs: vrací Boolean. Neexistující výstupní adresář, cesta se znaky, které operační systém odmítá, nebo stav plného disku způsobí, že SaveAs vrátí False bez vyvolání výjimky. V dávkové úloze, která rozdělí 200stránkový dokument na 200 jednostránkových souborů, je tiché selhání na stránce 147 snadné přehlédnout. Zkontrolujte návratovou hodnotu při každém volání a spočítejte úspěchy oproti očekávanému celkovému počtu po skončení smyčky
Zde uvedené metody ImportPages a CreateDocument jsou součástí komponenty PDFium pro Delphi a C++Builder