PDFium Component gibt Ihnen genau eine Methode zum Aufteilen von PDF-Dateien: ImportPages. Alles andere, ob Sie nun eine einzelne Seite herauslösen, an beliebigen Grenzen schneiden oder der Lesezeichenstruktur des Dokuments folgen, sind nur unterschiedliche Arten zu entscheiden, welche Seitenzahlen in welche Ausgabedatei wandern. Der Mechanismus bleibt derselbe. Das früh zu verstehen erspart viele Umwege
Wie die Aufteilungsschleife funktioniert
Das Muster ist dasselbe, unabhängig davon, wie Sie das Quelldokument unterteilen. Erzeugen Sie eine frische TPdf-Instanz, rufen Sie darauf CreateDocument auf, um ein leeres PDF im Speicher zu initialisieren, importieren Sie die gewünschten Seiten mit ImportPages, speichern Sie das Ergebnis und setzen Sie dann Active vor dem nächsten Durchlauf auf False zurück. Diesen letzten Schritt übersehen die Leute: CreateDocument schließt das noch im Speicher befindliche Dokument nicht implizit, also müssen Sie Ihre Ausgabe speichern und Active := False ausdrücklich zurücksetzen, bevor Sie es erneut aufrufen; zuerst zurückzusetzen hält den Zustand sauber und wohldefiniert. Die äußere TPdf-Instanz wird über alle Durchläufe hinweg wiederverwendet, was den Allokationsdruck bei großen Aufträgen niedrig hält
So sieht das seitenweise Aufteilen auf das Wesentliche reduziert aus:
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 ist eine 1-basierte Seitenzahl-Zeichenkette; Einfügeposition 1 = erste 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; // Zurücksetzen vor dem nächsten CreateDocument
end;
finally
PdfOut.Free;
end;
end;
Der Parameter Range von ImportPages hat dasselbe Zeichenkettenformat, das PDFium intern verwendet: eine kommagetrennte Liste von Seitenzahlen oder mit Bindestrich getrennten Bereichen, alle 1-basiert. '3' importiert Seite 3. '1-5' importiert die Seiten 1 bis 5 in dieser Reihenfolge. '2,5,8' importiert diese drei Seiten. Der dritte Parameter ist die 1-basierte Einfügeposition im Zieldokument; die Übergabe von 1 platziert importierte Seiten immer am Anfang einer ansonsten leeren Datei, und genau das wollen Sie hier
Nach Seitenbereichen aufteilen
Wenn der Aufrufer eine Liste wie 1-12,13-24,25-36 liefert, zerlegen Sie sie in Start-/Endpaare und lassen dieselbe Schleife laufen, wobei Sie die Bereichszeichenkette aus jedem Paar bilden:
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;
Die Prüfung, bevor Sie bei ImportPages ankommen, zählt hier. ImportPages gibt False zurück, wenn eine Seitenzahl in der Bereichszeichenkette Source.PageCount übersteigt, aber es löst keine Ausnahme aus und erzeugt keine unvollständige Ausgabedatei, die Sie am Namen allein erkennen könnten. Prüfen Sie den Rückgabewert von SaveAs und protokollieren Sie Fehlschläge gesondert; ein Bereich, der eine leere Ausgabedatei erzeugt, fällt erst auf, wenn jemand sie öffnet
An Lesezeichengrenzen aufteilen
Der dritte Ansatz nutzt die Struktur des Dokuments selbst statt einer extern gelieferten Liste. Jedes Lesezeichen der obersten Ebene trägt eine Zielseitenzahl; der Abschnitt, den es definiert, reicht von dieser Seite bis eine Seite vor der Seite des nächsten Lesezeichens, beim letzten Eintrag bis zum Ende des Dokuments
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; // Fehlerhaften Abschnitt überspringen, statt eine leere Datei zu schreiben
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;
Ein Dokument ohne Lesezeichen ist kein Fehlerzustand, den man dem Benutzer als solchen zeigen müsste; es bedeutet nur, dass dieser Aufteilungsmodus nichts hat, worauf er aufbauen kann. Die Absicherung Length(Bm) = 0 erledigt das stillschweigend. Zeigen sollte man dagegen, wenn die Seitenzahl eines Lesezeichens außerhalb des Dokumentbereichs liegt, was in fehlerhaften Dateien vorkommt, deren Gliederung nach dem Löschen von Seiten nie aktualisiert wurde. Die Bereichsprüfung von StartPage und EndPage überspringt diese Einträge, statt einen unsinnigen Bereich an ImportPages zu übergeben
Benennung der Ausgabedateien und der Active-Reset
Die Dateinamensicherheit bei aus Lesezeichen abgeleiteten Namen braucht ausdrückliche Aufmerksamkeit. Lesezeichentitel können Zeichen enthalten, die in einer PDF-Zeichenkette gültig sind, in einem Dateisystempfad aber nicht. Ersetzen Sie mindestens Schrägstrich, Rückwärtsschrägstrich und Doppelpunkt, bevor Sie den Ausgabepfad bilden. Unter Windows sind außerdem *, ?, ", <, > und | verboten; eine einfache Schleife über eine feste Menge deckt sie ab, ohne dass Sie einen regulären Ausdruck hinzuziehen müssen
Die Zeile Active := False am Ende jedes Durchlaufs verdient Betonung, weil sie die einzige nicht offensichtliche Anforderung des Musters ist. CreateDocument schließt nicht implizit, was gerade offen ist. Ist Active noch True, wenn CreateDocument erneut läuft, wurde das noch im Speicher liegende Dokument nie ordentlich geschlossen oder gespeichert, und in diesem Zustand können Sie sich nicht auf wohldefiniertes Verhalten verlassen, also speichern und zurücksetzen Sie ausdrücklich, bevor Sie das nächste Dokument beginnen. Betrachten Sie es als Gegenstück zu try/finally: Der finally-Block gibt das äußere Objekt frei; das Active := False setzt den inneren Dokumentzustand zwischen den Schleifendurchläufen zurück
Der Speicherverbrauch bleibt bei diesem Ansatz über einen großen Aufteilungsauftrag hinweg flach, weil Sie nie mehr als ein Ausgabedokument gleichzeitig im Speicher halten. Das Quelldokument bleibt durchgehend geöffnet und schreibgeschützt; ImportPages kopiert Seitendaten in das neue Dokument, ohne die Quelle zu verändern. Ist die Quelle verschlüsselt, öffnen Sie sie vor der Schleife mit ihrem Passwort, und die kopierten Seiten in jeder Ausgabedatei sind unverschlüsselt, was für an verschiedene Empfänger verteilte Teilausgaben meist das richtige Verhalten ist
Noch etwas zu SaveAs: Es gibt einen Boolean zurück. Ein nicht vorhandenes Ausgabeverzeichnis, ein Pfad mit Zeichen, die das Betriebssystem ablehnt, oder eine volle Festplatte führen alle dazu, dass SaveAs ohne Ausnahme False zurückgibt. In einem Stapelauftrag, der ein 200-seitiges Dokument in 200 Einzelseitendateien zerlegt, wird ein stiller Fehlschlag bei Seite 147 leicht übersehen. Prüfen Sie den Rückgabewert bei jedem Aufruf und gleichen Sie am Ende der Schleife die Zahl der Erfolge mit dem erwarteten Gesamtwert ab
Die hier gezeigten Methoden ImportPages und CreateDocument sind Teil von PDFium Component für Delphi und C++Builder