Technischer Artikel

Zusammenführen mehrerer PDF-Dateien zu einem Dokument mit der PDFium-Komponente

Die PDFium-Komponente bietet das Zusammenführen von PDFs über eine einzige Methode an: ImportPages. Das Muster ist immer dasselbe: Erstellen Sie ein leeres Zieldokument, öffnen Sie jede Quelldatei, rufen Sie ImportPages auf, um die Seiten zu kopieren, schließen Sie die Quelle und wiederholen Sie den Vorgang. Wenn die Schleife endet, schreibt SaveAs das Ergebnis auf die Festplatte. Es gibt keinen speziellen Zusammenführungsmodus, keine Konfiguration zum Umschalten. Die Komplexität liegt in den Grenzfällen, und es gibt einige, die ohne Vorwarnung zuschlagen

Die Kernschleife

Zwei TPdf-Instanzen sind alles, was Sie brauchen. Eine hält das Zieldokument, das mit CreateDocument leer erstellt wird. Die andere öffnet nacheinander jede Quelldatei. Unten sehen Sie eine Prozedur, die eine Liste von Dateipfaden annimmt und die zusammengeführte Ausgabe in einen einzigen Pfad schreibt:

procedure MergeFiles(const FileList: TStrings; const OutputPath: string);
var
  PdfDest, PdfSrc: TPdf;
  InsertAt, I: Integer;
begin
  PdfDest := TPdf.Create(nil);
  PdfSrc  := TPdf.Create(nil);
  try
    PdfDest.CreateDocument;
    InsertAt := 1;  // ImportPages uses 1-based destination position

    for I := 0 to FileList.Count - 1 do
    begin
      PdfSrc.FileName := FileList[I];
      PdfSrc.Active   := True;

      if not PdfSrc.Active then
        raise Exception.CreateFmt('Cannot open: %s', [FileList[I]]);

      PdfDest.ImportPages(
        PdfSrc,
        '1-' + IntToStr(PdfSrc.PageCount),  // full document range
        InsertAt);

      Inc(InsertAt, PdfSrc.PageCount);
      PdfSrc.Active := False;
    end;

    PdfDest.SaveAs(OutputPath);
  finally
    PdfSrc.Free;
    PdfDest.Free;
  end;
end;

Zwei Dinge in diesem Code werden beim ersten Lesen leicht übersehen. Das erste ist, wie PDFium Ladefehler meldet. Active := True löst niemals eine Ausnahme aus: Wenn die Datei fehlt, beschädigt oder passwortgeschützt ist, fängt PDFium den Fehler intern ab und belässt Active auf False. Ohne die explizite Prüfung in Zeile 10 würde eine fehlerhafte Datei ohne jeden Hinweis in der Ausgabe stillschweigend aus dem Zusammenführen herausfallen. Das fertige PDF hätte weniger Seiten als erwartet, und Sie wüssten nicht, welche Datei der Übeltäter war

Das zweite ist der InsertAt-Zähler. Das dritte Argument für ImportPages ist die 1-basierte Position im Ziel, an der die erste importierte Seite landet. Wenn Sie bei 1 beginnen, wird das erste Quelldokument an den Anfang einer ansonsten leeren Datei gesetzt. Nach jeder Quelle rückt der Zähler um PdfSrc.PageCount vor, sodass der nächste Stapel von Seiten an die letzte angehängt wird. Vergessen Sie, ihn zu erhöhen, und jede nachfolgende Quelle überschreibt Seiten an Position 1, sodass Sie nur das letzte Dokument in der Liste erhalten und sonst nichts

Selektive Seitenbereiche

Sie müssen nicht jede Seite aus einer Quelle übernehmen. Der als zweites Argument übergebene Bereichsstring folgt einem einfachen Komma-und-Bindestrich-Format: "1-3" übernimmt die Seiten 1 bis 3, "2,4,6" wählt drei bestimmte Seiten aus und "1-" bedeutet Seite 1 bis zum Ende des Dokuments. Bereiche können in einem einzigen String kombiniert werden, sodass "1-3,5,7-" die Seiten 4 und 6 überspringt. Eine Feinheit ist hierbei wichtig: Die Zahlen beziehen sich immer auf Seiten im Quelldokument, beginnend bei 1, unabhängig davon, wo diese Seiten im Ziel landen. Wenn Sie die Seiten 40 bis 50 aus einem 200-seitigen Katalog möchten, lautet der Bereichsstring "40-50", nicht eine Position relativ zu dem, was sich bereits im Ziel befindet

// Extract cover plus a three-page executive summary from a long report
PdfSrc.FileName := 'annual-report.pdf';
PdfSrc.Active   := True;
if PdfSrc.Active then
begin
  // Page 1 is the cover; pages 3-5 are the summary
  PdfDest.ImportPages(PdfSrc, '1,3-5', InsertAt);
  Inc(InsertAt, 4);  // 1 cover + 3 summary pages = 4 pages added
  PdfSrc.Active := False;
end;

Beim Berechnen der Schrittweite für InsertAt zählen Sie die Seiten, die Sie tatsächlich importiert haben, nicht die Seitenanzahl der Quelle. Wenn Sie '1,3-5' übergeben, haben Sie 4 Seiten importiert, also erhöhen Sie um 4. Das Erhöhen um PdfSrc.PageCount würde eine Lücke von leeren Zielpositionen hinterlassen und das nächste Quelldokument weiter im Dokument platzieren als beabsichtigt

Was ImportPages bewahrt und was nicht

Seiten, die von ImportPages kopiert wurden, behalten ihren sichtbaren Inhalt intakt bei. Text, Vektorgrafiken, Rasterbilder, eingebettete Schriftarten und Formular-XObjects werden alle als Teil der Seiteninhaltsströme übertragen. Anmerkungen auf Seitenebene, einschließlich Kommentaren, Hervorhebungen und Tintenstrichen, werden ebenfalls übernommen, da sie im Seitenwörterbuch und nicht auf Dokumentebene gespeichert sind

Bei Metadaten auf Dokumentebene sieht es anders aus. Der Titel, der Autor, das Thema und die Schlüsselwort-Strings im Info-Wörterbuch der Quelle bleiben zurück. Das Zieldokument startet nach CreateDocument mit leeren Metadaten. Wenn die zusammengeführte Ausgabe diese Felder gefüllt haben muss, müssen Sie sie PdfDest direkt zuweisen, bevor Sie SaveAs aufrufen. Die Eigenschaften Title, Author, Subject, Keywords und Creator von TPdf nehmen einfache Strings auf und schreiben sie beim Speichern in das Info-Wörterbuch

Interaktive Formularfelder sind komplizierter. AcroForm-Felddefinitionen leben in einem Wörterbuch auf Dokumentebene, nicht innerhalb einzelner Seitenströme. Wenn ImportPages eine Seite kopiert, die Formularfelder enthält, wird das visuelle Erscheinungsbild dieser Felder übertragen, weil es in den Seiteninhaltsstrom gerendert wird, aber die Feld-Widgets, die sie interaktiv machen, sind Teil der AcroForm-Struktur und folgen nicht. Bei einem typischen Zusammenführen zeigt ein Textfeld aus einem Quelldokument den Wert an, den es zum Zeitpunkt des Imports hatte, aber es kann in der zusammengeführten Datei nicht bearbeitet werden. Wenn Sie möchten, dass die Felder weiterhin ausfüllbar bleiben, müssen Sie sie in jedem Quelldokument reduzieren (flatten), bevor Sie sie importieren: Dadurch werden die aktuellen Werte in den Inhaltsstrom festgeschrieben und das interaktive Overlay entfernt, was Ihnen ein sauberes visuelles Ergebnis ohne defekte Widgets in der Ausgabe liefert

Verschlüsselte Quelldateien

Passwortgeschützte Quelldokumente lassen sich genauso öffnen wie unverschlüsselte, mit einer zusätzlichen Eigenschaft, die zuerst gesetzt werden muss. Weisen Sie das Passwort PdfSrc.Password zu, bevor Sie Active := True umschalten, und PDFium wird es während des Öffnens verwenden:

PdfSrc.Password := 'user-password';
PdfSrc.FileName := 'protected.pdf';
PdfSrc.Active   := True;
if not PdfSrc.Active then
  raise Exception.Create('Wrong password or file cannot be opened');

PdfDest.ImportPages(PdfSrc, '1-' + IntToStr(PdfSrc.PageCount), InsertAt);
Inc(InsertAt, PdfSrc.PageCount);
PdfSrc.Active := False;

Ein falsches Passwort führt zum selben stillschweigenden Active = False-Ergebnis wie eine fehlende Datei, weshalb die explizite Prüfung auch hier unerlässlich ist. Die Verschlüsselung wird nicht auf das Ziel übertragen: Aus einer geschützten Quelle importierte Seiten landen als ungeschützter Inhalt im Ziel. Wenn die zusammengeführte Ausgabe ebenfalls eine Verschlüsselung benötigt, konfigurieren Sie sie in PdfDest, bevor Sie SaveAs aufrufen

Das Ergebnis speichern

SaveAs in TPdf akzeptiert entweder einen Dateipfad oder einen TStream. Für die meisten Zusammenführungen ist die Datei-Überladung das, was Sie wollen:

PdfDest.SaveAs('merged-output.pdf');

Das optionale zweite Argument ist eine TSaveOption, die den Speichermodus steuert. Der Standardwert saNone schreibt ein inkrementelles Update, wenn das Dokument aus einer Datei geladen wurde, oder ein vollständiges Umschreiben, wenn es neu erstellt wurde. Da ein mit CreateDocument erstelltes Ziel immer neu ist, wird die Ausgabe eine kompakte Datei mit nur einer Revision sein. Das dritte Argument, TPdfVersion, ermöglicht es Ihnen, den PDF-Versions-Header festzulegen, wenn nachgelagerte Konsumenten eine bestimmte Version erfordern; wenn Sie ihn auf pvUnknown belassen, wählt PDFium basierend auf dem Inhalt aus

Die hier gezeigten Methoden ImportPages und SaveAs sind Teil der PDFium-Komponente für Delphi und C++Builder