PDFium Component geeft u één methode om een PDF te splitsen: ImportPages. Al het andere, of u nu één pagina isoleert, op willekeurige grenzen knipt of de eigen bladwijzerstructuur van het document volgt, is niet meer dan een andere manier om te bepalen welke paginanummers in welk uitvoerbestand belanden. De mechaniek blijft dezelfde. Dat vroeg doorhebben bespaart een hoop verkeerde afslagen
Hoe de splitslus werkt
Het patroon is hetzelfde ongeacht hoe u het brondocument verdeelt. Maak een verse instantie van TPdf, roep daarop CreateDocument aan om een lege PDF in het geheugen te initialiseren, importeer de gewenste pagina's met ImportPages, sla het resultaat op, en zet Active vóór de volgende iteratie terug op False. Die laatste stap is degene die men mist: CreateDocument sluit het document dat nog in het geheugen zit niet impliciet, dus u moet uw uitvoer opslaan en Active := False expliciet terugzetten voordat u het opnieuw aanroept; eerst resetten houdt de toestand schoon en welgedefinieerd. De buitenste instantie van TPdf wordt over alle iteraties hergebruikt, wat de druk van geheugentoewijzing bij grote taken laag houdt
Zo ziet splitsen per pagina eruit wanneer het tot de kern is teruggebracht:
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 een 1-gebaseerde tekenreeks met paginanummers; invoegpunt 1 = eerste positie
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 voor de volgende CreateDocument
end;
finally
PdfOut.Free;
end;
end;
De parameter Range van ImportPages heeft dezelfde tekenreeksvorm die PDFium intern gebruikt: een door komma's gescheiden lijst van paginanummers of met een koppelteken afgebakende bereiken, allemaal 1-gebaseerd. '3' importeert pagina 3. '1-5' importeert de pagina's 1 tot en met 5 op volgorde. '2,5,8' importeert die drie pagina's. De derde parameter is de 1-gebaseerde invoegpositie in het doeldocument; 1 doorgeven plaatst de geïmporteerde pagina's altijd aan het begin van een verder leeg bestand, en dat is wat u hier wilt
Splitsen op paginabereiken
Levert de aanroeper een lijst als 1-12,13-24,25-36, dan parseert u die tot paren van begin en eind en draait u dezelfde lus, waarbij u uit elk paar de bereiktekenreeks opbouwt:
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;
Validatie voordat u bij ImportPages aankomt telt hier. ImportPages geeft False terug wanneer een paginanummer in de bereiktekenreeks groter is dan Source.PageCount, maar het werpt geen uitzondering op en het levert geen gedeeltelijk uitvoerbestand op dat u aan de naam alleen kunt herkennen. Controleer de retourwaarde van SaveAs en log mislukkingen apart; een bereik dat een leeg uitvoerbestand oplevert, is pas duidelijk fout wanneer iemand het opent
Splitsen op bladwijzergrenzen
De derde aanpak gebruikt de eigen structuur van het document in plaats van een extern aangeleverde lijst. Elke bladwijzer op het hoogste niveau draagt een doelpaginanummer; de sectie die hij afbakent, loopt van die pagina tot één pagina vóór de pagina van de volgende bladwijzer, of tot het einde van het document bij de laatste vermelding
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; // sla een misvormde sectie over in plaats van een leeg bestand te schrijven
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;
Een document zonder bladwijzers is geen foutsituatie die het waard is als zodanig aan de gebruiker te tonen; het betekent alleen dat deze splitsmodus niets heeft om mee te werken. De bewaking Length(Bm) = 0 handelt dat stilzwijgend af. Wat wél het tonen waard is, is een bladwijzer waarvan het paginanummer buiten het bereik van het document valt, wat gebeurt in misvormde bestanden waarin de overzichtsstructuur nooit is bijgewerkt nadat er pagina's waren verwijderd. De grenscontrole op StartPage en EndPage slaat die vermeldingen over in plaats van een onzinnig bereik aan ImportPages door te geven
Uitvoerbestanden benoemen en het resetten van Active
De veiligheid van bestandsnamen die uit bladwijzers zijn afgeleid vraagt expliciete aandacht. Bladwijzertitels kunnen tekens bevatten die in een PDF-tekenreeks geldig zijn maar niet in een pad in het bestandssysteem. Vervang minstens de schuine streep, de omgekeerde schuine streep en de dubbele punt voordat u het uitvoerpad opbouwt. Op Windows zijn ook *, ?, ", <, > en | verboden; een eenvoudige lus over een vaste verzameling dekt die zonder dat u een reguliere expressie binnenhaalt
De regel Active := False aan het einde van elke iteratie verdient nadruk, want zij is de enige niet voor de hand liggende eis in het patroon. CreateDocument sluit niet impliciet wat er open staat. Is Active nog True wanneer CreateDocument opnieuw draait, dan is het document dat nog in het geheugen zit nooit netjes gesloten of opgeslagen, en op welgedefinieerd gedrag kunt u in die toestand niet rekenen, dus sla expliciet op en reset voordat u aan het volgende document begint. Zie het als de tegenhanger van try/finally: het finally-blok geeft het buitenste object vrij; de Active := False reset de toestand van het binnenste document tussen de lusiteraties
Het geheugengebruik over een grote splitstaak blijft met deze aanpak vlak, want u houdt nooit meer dan één uitvoerdocument tegelijk in het geheugen. Het brondocument blijft de hele tijd geopend en alleen-lezen; ImportPages kopieert paginagegevens naar het nieuwe document zonder de bron te wijzigen. Is de bron versleuteld, open hem dan vóór de lus met zijn wachtwoord en de gekopieerde pagina's in elk uitvoerbestand zijn onversleuteld, wat meestal het juiste gedrag is voor gesplitste uitvoer die naar verschillende ontvangers gaat
Nog iets over SaveAs: het geeft een Boolean terug. Een uitvoermap die niet bestaat, een pad met tekens die het besturingssysteem weigert, of een volle schijf zorgen er alle voor dat SaveAs False teruggeeft zonder een uitzondering op te werpen. In een batchtaak die een document van 200 pagina's in 200 losse bestanden splitst, is een stille mislukking op pagina 147 makkelijk over het hoofd te zien. Controleer de retourwaarde bij elke aanroep en tel na afloop van de lus de geslaagde pogingen af tegen het verwachte totaal
De hier getoonde methoden ImportPages en CreateDocument maken deel uit van PDFium Component voor Delphi en C++Builder