Technischer Artikel

Gigabyte-PDFs in Delphi mit PDFlibPas Direct Access zusammenführen und aufteilen

Der nächtliche Job, der Hypothekenabschluss-Pakete zusammensetzte, lief zwei Jahre lang problemlos und starb dann in der Woche, in der ein Scan-Dienstleister auf 600 DPI umstellte. Einzelne Archivdateien überschritten 1,8 GB, und der Montageschritt — der jedes Dokument vollständig lud, bevor er eine einzige Seite berührte — erschöpfte den Adressraum des Workers schon beim Seitenzählen. Die Anforderung hatte sich nicht geändert: öffnen, zählen, Bereiche wählen, verketten. Geändert hatte sich, dass vollständiges Baumladen irgendwo um die Gigabyte-Marke nicht mehr die vernünftige Vorgabe ist. PDFlibPas, losLabs PDF-Bibliothek für Delphi und C++Builder, adressiert genau das mit seiner Direct-Access-Schicht: einer Familie DA-präfixierter Funktionen, die auf einem Streaming-Reader aufbauen, der die Cross-Reference-Tabelle an Ort und Stelle durchläuft, statt das Dokument zu materialisieren.

Wo der Speicher bei einem vollständigen Laden hingeht

Ein PDF „normal“ zu laden bedeutet, das XRef zu parsen, jedes indirekte Objekt in einen In-Memory-Baum aufzulösen, Objektstreams zu dekodieren und Seitenbaum, Schriften und Annotationen als manipulierbare Objekte zu verdrahten. Für Bearbeitungsworkflows ist das der richtige Tausch. Für Merge-, Split- und Inspektionsworkloads ist es meist Verschwendung: Ein Scan-Archiv mit 30.000 Seiten kann Millionen indirekter Objekte enthalten, von denen ein Split-Job nur einige Hundert lesen muss — die Seitenknoten im angeforderten Bereich und das, worauf sie verweisen.

Die Direct-Access-Schicht dreht das Modell um. DAOpenFile und DAOpenFileReadOnly parsen Trailer und XRef — ein paar Kilobyte am Dateiende — und geben ein Datei-Handle zurück. Objekte werden erst geholt, wenn ein Aufruf sie braucht. Die praktische Folge ist: Das Öffnen einer mehrere Gigabyte großen Datei dauert ungefähr so lang wie das Öffnen einer kleinen, und der Speicher folgt dem, was Sie berühren, nicht dem, was die Datei enthält.

Eine riesige Datei prüfen, ohne sie zu laden

Das folgende Muster stammt aus dem Large-File-Benchmark der Bibliothek: read-only öffnen, Fragen stellen, schließen. Ein Dokumentbaum existiert nie.

var
  Lib: TPDFlib;
  Handle, Pages: Integer;
begin
  Lib := TPDFlib.Create;
  try
    Handle := Lib.DAOpenFileReadOnly('archive-2025.pdf', '');
    if Handle = 0 then
      raise Exception.Create('Direct access open failed');
    Pages := Lib.DAGetPageCount(Handle);
    Writeln('pages : ', Pages);
    Writeln('title : ', Lib.DAGetInformation(Handle, 'Title'));
    Lib.DACloseFile(Handle);
  finally
    Lib.Free;
  end;
end;

Read-only-Modus lohnt sich immer dann, wenn er möglich ist: Er lässt die Ingest-Stufe laufen, während andere Prozesse die Datei halten, und dokumentiert Absicht — eine Prüfphase, die versehentlich eine mutierende Funktion aufruft, fällt schnell aus, statt das Archiv zu beschädigen.

PageRef ist ein Objekt-Handle, keine Seitennummer

Der häufigste Fehler mit der DA-API ist, eine Seitennummer zu übergeben, wo eine Funktion ein PageRef erwartet. Fast jeder seitenbezogene DA-Aufruf — DAExtractPageText, DARenderPageToFile, DARotatePage, DACapturePage — nimmt ein Referenz-Handle auf das Seitenobjekt, das durch Übersetzen der menschenlesbaren Nummer über DAFindPage gewonnen wird:

PageRef := Lib.DAFindPage(Handle, 250);          // page number -> object handle
if PageRef <> 0 then
begin
  Text := Lib.DAExtractPageText(Handle, PageRef, 0);
  Lib.DARenderPageToFile(Handle, PageRef, 5, 150, 'page250.png');
end;

Die rohe Zahl 250 zu übergeben wirft keinen Fehler — sie adressiert, welches Objekt zufällig hinter diesem Handle-Wert liegt. An einem glücklichen Tag fällt das sichtbar aus, an einem schlechten extrahiert es Text aus der falschen Seite in ein kundenrelevantes Dokument. Wenn Sie die DA-Schicht in eigenen Servicecode kapseln, machen Sie das Überspringen der Übersetzung unmöglich: Nehmen Sie an der Grenze Seitennummern an, rufen Sie sofort DAFindPage auf und reichen Sie intern nur Refs weiter.

Hunderte Dateien mit einer benannten Liste zusammenführen

Für zwei Dateien reicht MergeFiles(First, Second, Output). Batch-Montage skaliert besser über Dateilisten: Eingaben unter einem Listennamen registrieren, dann die Liste in einem Durchlauf mergen.

Lib.AddToFileList('Statements', 'jan.pdf');
Lib.AddToFileList('Statements', 'feb.pdf');
Lib.AddToFileList('Statements', 'mar.pdf');
Lib.MergeFileList('Statements', 'q1-statements.pdf');

// Verify the result the cheap way: direct access again
Handle := Lib.DAOpenFileReadOnly('q1-statements.pdf', '');
Writeln('merged pages: ', Lib.DAGetPageCount(Handle));
Lib.DACloseFile(Handle);

Die Merge-Familie hat drei Varianten, und der Unterschied ist nicht nur Geschwindigkeit. MergeFileListFast überspringt die Erhaltung des Strukturbaums; MergeFileListStrict erzwingt Strict Mode; die unsuffigierte Version ist der ausgewogene Standard. Daraus folgt die Betriebsregel: Wenn eine Eingabe ein Tagged PDF ist, dessen Accessibility-Struktur erhalten bleiben muss — etwa alles, was für PDF/UA erzeugt wurde —, verwenden Sie die Standard- oder Strict-Variante, denn Fast verwirft den Strukturbaum still. Für reine Scan-Archive ohne Tagging ist Fast kostenlose Leistung. Entscheiden Sie pro Pipeline, nicht nach Entwicklerlaune, und protokollieren Sie die verwendete Variante im Joblog.

Aufteilen ohne Laden: Bereichsextraktion

Splitting folgt derselben No-Load-Philosophie. ExtractFilePages(InputFileName, Password, OutputFileName, RangeList) zieht einen Seitenbereich direkt von Datei zu Datei — '1-500', '501-1000' oder kommaseparierte Auswahlen —, ohne dass die Quelle je ein Dokumentbaum wird. Wenn ein Dokument aus anderen Gründen bereits geladen ist, erzeugt ExtractPageRanges ein neues In-Memory-Dokument aus dem aktuellen, und CopyPageRanges zieht Bereiche aus einem anderen geladenen Dokument per ID herüber. Für das Aufteilen konsolidierter Druckströme pro Auszug ist die Datei-zu-Datei-Form diejenige, die eine 4 GB-Eingabe nie in RAM aufbläht.

Dateien, die über ihre Geometrie lügen

Large-File-Pipelines treffen beschädigte Dateien häufiger als Small-File-Pipelines, schlicht weil die Eingaben durch mehr Systeme laufen. Zwei Fehlertypen verdienen explizite Behandlung.

Erstens verschobene Header. Mail-Gateways und Druckspooler stellen einem PDF manchmal Bytes voran, sodass der %PDF-Marker nicht mehr bei Offset 0 sitzt und jeder XRef-Offset in der Datei um denselben Betrag falsch ist. Der Streaming-Reader erkennt das und legt es offen — DAShiftedHeader auf flacher Ebene, ShiftedHeader auf TSmartPDFReader — und kompensiert beim Lesen. Selbstgebaute Offset-Arithmetik tut das normalerweise nicht, weshalb „funktioniert mit jeder Datei, die wir erzeugen, scheitert bei Dateien von Kunde X“ das klassische Symptom ist.

Zweitens defekte Cross-Reference-Tabellen. DACopyFile(InputFileName, OutputFileName, PageCount) streamt die ganze Datei in eine neue Kopie und baut dabei das XRef neu auf, wobei die Seitenzahl als Nebenprodukt zurückkommt. Als Normalisierungsstufe vor einem wählerischen Downstream-Consumer verwandelt es eine Klasse intermittierender Parsefehler in einen vorhersehbaren Reparaturschritt. Und wenn eigene Änderungen gespeichert werden müssen, schreibt DAAppendFile sie als inkrementelles Update — es hängt eine neue Revision an, statt Gigabytes neu zu schreiben, sodass die Speicherkosten proportional zur Änderung bleiben, nicht zur Datei.

Auslieferungsdetails: Linearisierung und Komposition

Zwei benachbarte Fähigkeiten runden eine Large-File-Pipeline ab. Wenn die zusammengesetzte Ausgabe für die Anzeige im Browser über HTTP ausgeliefert wird, reorganisiert LinearizeFile sie für Byte-Range-Streaming, sodass die erste Seite erscheint, bevor der Rest eines 500 MB-Pakets geladen ist — das lohnt sich als letzte Stufe nach allen Merges, weil jede spätere Änderung die Linearisierung wieder aufhebt. Und wenn Pakete Komposition statt bloßer Verkettung brauchen — ein Deckblatt hinter jedem Auszug gestempelt, zwei Quellseiten auf ein Ausgabeblatt ausgeschossen —, macht DACapturePage jede Seite zu einer wiederverwendbaren Vorlage, die DADrawCapturedPage auf einer Zielseite in ein beliebiges Rechteck setzt, weiterhin ohne vollständiges Laden der mehrere Gigabyte großen Quelle.

Häufige Fragen zu großen Dateien

Wie groß darf eine Datei für Direct Access sein? Offsets sind in der DA-Schicht durchgehend Int64, also ist nicht die Formatgrenze das Problem, sondern verfügbarer Speicherplatz und die 10-stellige XRef-Offset-Grenze klassischer PDFs. In der Praxis sind Multi-Gigabyte-Scanarchive Routine; der Speicher bleibt begrenzt, weil Objekte bei Bedarf geholt werden.

Erhält das Zusammenführen Lesezeichen und Links? Der Standard-Merge-Pfad übernimmt die Dokumentstruktur; die Fast-Variante tauscht Strukturbaum-Erhaltung gegen Geschwindigkeit. Prüfen Sie mit echten Eingaben: Ausgabe öffnen, Outline durchlaufen und interne Links stichprobenartig testen — ein Fünf-Minuten-Test, der schon viele lange Support-Threads beendet hat.

Kann ich über Direct Access bearbeiten oder nur lesen? Ein nützlicher Mittelweg existiert: Seitenoperationen wie DARotatePage, DAMovePage, DAHidePage und Formularfeld-Lesezugriffe arbeiten auf dem Handle, und DAAppendFile persistiert sie inkrementell. Inhaltsbearbeitung gehört weiterhin in die vollständige Dokumentebene.

Verwandte Artikel

Wenn Ihre gemergte Ausgabe barrierefrei bleiben muss, behandelt der Artikel zu Tagged-PDF-Accessibility den Strukturbaum-Hintergrund — er erklärt, was die Fast-Merge-Variante verwerfen würde. Für das Herausziehen von Inhalt aus den Bereichen, die Sie splitten, siehe den Leitfaden zu Text-, Bild- und Schriftextraktion.

Die vollständige Direct-Access-Funktionsliste wird mit der Bibliothek ausgeliefert; Editionen und Testdownloads stehen auf der PDFlibPas-Produktseite.