Technischer Artikel

Inkrementelle PDF-Aktualisierungen in Delphi: AppendToStream-Anleitung

Inkrementelle PDF-Aktualisierungen ermöglichen es einer Delphi-Anwendung, ein Dokument zu ändern, indem nur die geänderten Objekte angehängt werden, wodurch jedes ursprüngliche Byte unberührt bleibt. Die losLab PDF Library implementiert dies über AppendToStream, das nur den in ISO 32000-1 §7.5.6 definierten inkrementellen Abschnitt schreibt. So kostet das Bearbeiten eines einzelnen Lesezeichens in einer 2 GB großen Datei nur wenige Kilobyte an Ausgabe statt eines vollständigen Umschreibens. Derselbe Mechanismus ist der Grund dafür, warum signierte Dokumente aktualisiert werden können, ohne dass ihre Signaturen ungültig werden

Das Problem, das hierdurch gelöst wird, ist konkret. Ein vollständiges Speichern schreibt die gesamte Datei neu: Jedes Objekt wird neu serialisiert, jeder Kreuzverweis-Offset neu berechnet, und die Ausgabe weist auf Byte-Ebene keine Beziehung zur Eingabe auf. Für eine 40 KB große Rechnung ist das in Ordnung. Für ein 2 GB großes gescanntes Archiv, bei dem Sie lediglich einen Tippfehler im Dokumenttitel korrigiert haben, ist das Umschreiben von zwei Gigabyte zur Änderung von zwanzig Byte absurd — und falls die Datei eine digitale Signatur trug, wurde diese durch das Umschreiben gerade zerstört

Warum bricht das Speichern eines PDFs seine digitale Signatur?

Eine digitale PDF-Signatur signiert nicht den logischen Inhalt des Dokuments; sie signiert Byte-Bereiche der physischen Datei. Der Eintrag /ByteRange im Signatur-Verzeichnis zeichnet genau auf, welche Abschnitte der Datei durch den kryptografischen Digest abgedeckt sind. Jedes Speichern, das diese Bytes neu serialisiert — selbst wenn dabei ein semantisch identisches Dokument entsteht — verändert den Digest, und jeder Validator wird die Signatur als beschädigt melden. Dies ist beabsichtigt: Die Signatur beglaubigt die Bytes, die der Unterzeichner gesehen hat, und nicht irgendein abstraktes Dokumentenmodell

Inkrementelle Aktualisierungen sind das Hintertürchen, das die PDF-Spezifikation bereitstellt. Da ein inkrementelles Speichern neue Daten nach dem ursprünglichen %%EOF anhängt und die signierten Byte-Bereiche niemals berührt, wird die bestehende Signatur weiterhin anhand der von ihr abgedeckten Bytes validiert. Validatoren stufen die angehängten Änderungen dann separat ein — eine zweite Signatur, ein ausgefülltes Formular, eine Anmerkung — und entscheiden, ob es sich um zulässige Änderungen handelt. Jeder Workflow mit mehreren Signaturen basiert darauf: Jeder Unterzeichner fügt einen inkrementellen Abschnitt auf dem vorherigen hinzu. Wenn Sie Signatur-Pipelines aufbauen, behandelt der Begleitartikel über PAdES-Signierung und -Validierung in Delphi im Detail, wie Signatur-Byte-Bereiche und inkrementelle Abschnitte zusammenwirken

Wie inkrementelle Aktualisierungen unter ISO 32000-1 §7.5.6 funktionieren

ISO 32000-1 §7.5.6 definiert das Modell in drei Regeln. Erstens bleibt der ursprüngliche Dateiinhalt vollständig intakt — kein einziges Byte wird verschoben. Zweitens werden geänderte und neu erstellte Objekte nach dem letzten %%EOF angehängt, jeweils mit derselben Objektnummer wie zuvor (geänderte Objekte erhalten einfach eine neuere Definition, die die alte überschattet). Drittens werden ein neuer Kreuzverweis-Abschnitt und ein Trailer angehängt; der Eintrag /Prev des Trailers verweist zurück auf den Byte-Offset des vorherigen Kreuzverweis-Abschnitts, wodurch eine Kette entsteht, die ein Reader von der neuesten zur ältesten durchläuft, um jedes Objekt zu seiner neuesten Definition aufzulösen

Aus dieser Struktur ergeben sich zwei nützliche Eigenschaften. Aktualisierungen sind proportional zu den Änderungen günstig, nicht zur Dokumentgröße — die Kosten für das Anhängen entsprechen der Größe der geänderten Objekte zuzüglich eines geringen Overheads für xref/Trailer. Und die Datei wird zu ihrem eigenen Versionsverlauf: Jede vorherige Revision ist physisch noch vorhanden, sodass ein Prüfer die Datei an jedem früheren %%EOF abschneiden und genau das Dokument wiederherstellen kann, das zu diesem Zeitpunkt existierte. Für Compliance-Workflows, die nachweisen müssen, wie ein Dokument vor jeder Änderung aussah, ist dieser integrierte Audit-Trail oft das entscheidende Argument für inkrementelles Speichern

Schreiben einer inkrementellen Aktualisierung mit AppendToStream

Die losLab PDF Library stellt die inkrementelle Ausgabe über AppendToStream(AppendMode: Integer; OutStream: TStream): Integer bereit, das bei Erfolg 1 und bei Fehlern 0 zurückgibt. Der Parameter AppendMode wählt aus, was im Ziel-Stream landet. Modus 0 schreibt eine vollständige Datei: Die ursprünglichen Quell-Bytes werden zuerst in den Stream kopiert, dann wird der inkrementelle Abschnitt angehängt. Modus 1 schreibt nur den inkrementellen Abschnitt selbst — das Delta — und überspringt die Quell-Bytes vollständig. Modus 2 schreibt zuerst ein vom Aufrufer bereitgestelltes Präfix, das über SetAppendInputFromString registriert wurde, und hängt dann den Aktualisierungsabschnitt darauf an

var
  Doc: TPDFlib;
  Delta: TMemoryStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('contract.pdf', '') <= 0 then
      Exit;

    // Kleine Bearbeitung: Die Art von Änderung, die kein
    // Umschreiben der gesamten Datei auslösen sollte
    Doc.SetInformation(3, 'Amended 2026-07-04');  // Schlüssel 3 = /Subject

    Delta := TMemoryStream.Create;
    try
      // AppendMode = 1: Nur den inkrementellen Abschnitt schreiben.
      // Ursprüngliche Bytes + Delta = ein vollständiges, gültiges PDF.
      if Doc.AppendToStream(1, Delta) = 1 then
        Delta.SaveToFile('contract.delta.bin');
    finally
      Delta.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Modus 1 ist der interessante für den Systementwurf. Da das Delta in sich geschlossen ist, können Sie es unabhängig vom Original versenden: Speichern Sie Revisionen als separate Blobs im Object Storage, replizieren Sie nur Deltas auf ein Remote-System oder rekonstruieren Sie jede Revision, indem Sie die Basisdatei mit ihrer Kette von Inkrementen verketten. Die Rekonstruktionsregel ist eine einfache Byte-Verkettung — zuerst die Originaldatei, dann jedes Delta in der richtigen Reihenfolge — da dies genau das Layout ist, das §7.5.6 für eine inkrementell aktualisierte Datei vorschreibt

Wie berechnet die Bibliothek xref-Offsets, ohne die Originaldatei zu kopieren?

Die Kreuzverweiseinträge innerhalb eines inkrementellen Abschnitts müssen absolute Byte-Offsets enthalten — Positionen, die vom Anfang der vollständigen Datei an gemessen werden, nicht vom Anfang des Deltas. Das stellt Modus 1 vor ein Rätsel: Der Writer gibt die Original-Bytes niemals aus, dennoch müssen alle aufgezeichneten Offsets so tun, als wären sie vorhanden. Die losLab PDF Library löst dies mit einem internen Stream-Adapter, TPDFAppendSectionStream, der dem Serialisierer einen virtuellen Koordinatenraum präsentiert. Der Adapter wird mit der Byte-Länge der Originaldatei als Basis-Offset erstellt, meldet seine Position und Größe als diese Basis plus alles, was bisher angehängt wurde, und leitet nur die neu geschriebenen Bytes an den Ziel-Stream des Aufrufers weiter

Die Folge ist, dass Modus 1 niemals eine Kopie des Quelldokuments materialisiert — weder auf der Festplatte noch im Speicher. Die naive Implementierung (die vollständige Datei in einen temporären Puffer schreiben und dann das Ende abschneiden) würde eine transiente Kopie des gesamten Original-PDFs erfordern, was bei Eingaben im Gigabyte-Bereich genau die Kosten verursacht, die durch inkrementelle Aktualisierungen vermieden werden sollen. Diese Offset-Virtualisierungstechnik ist eng verwandt mit der Byte-Referenz-Verschiebung, die an anderer Stelle in der Bibliothek verwendet wird; der Artikel über schnelles PDF-Zusammenführen mit Byte-Referenz-Verschiebung zeigt dieselbe Idee angewendet auf das Kombinieren von Dokumenten, und der Leitfaden für das Zusammenführen und Aufteilen großer PDFs mit direktem Dateizugriff behandelt die I/O-Architektur für Dateien, die nicht bequem in den RAM passen

Vollständiges Speichern streamen mit SaveToStream

Die inkrementelle Ausgabe ist die eine Hälfte des Streaming-Ansatzes; die andere Hälfte betrifft das, was bei einem vollständigen Speichern geschieht. SaveToStream in der losLab PDF Library treibt den Dokumentenserialisierer direkt gegen den Ziel-Stream, anstatt das gesamte Dokument zuerst in einen temporären AnsiString zu rendern und diesen Puffer dann in einem einzigen Aufruf herauszuschreiben. Der ältere Ansatz funktionierte zwar, bedeutete jedoch, dass bei jedem vollständigen Speichern vorübergehend eine zweite vollständige Kopie der Ausgabe im Speicher gehalten wurde — harmlos bei 10 MB, schmerzhaft bei 500 MB und eine harte Grenze für Ausgaben im Multi-Gigabyte-Bereich bei 32-Bit-Prozessen. Die direkte Serialisierung sorgt dafür, dass die Speicherspitze den Objektstrukturen des Dokuments folgt und nicht seiner serialisierten Länge

var
  Doc: TPDFlib;
  Output: TFileStream;
begin
  Doc := TPDFlib.Create;
  try
    if Doc.LoadFromFile('archive.pdf', '') <= 0 then
      Exit;

    // ... Bearbeitungen, die ein vollständiges Umschreiben rechtfertigen ...

    Output := TFileStream.Create('archive-rewritten.pdf', fmCreate);
    try
      if Doc.SaveToStream(Output) = 0 then
        Writeln('Save failed, error ', Doc.LastErrorCode);
    finally
      Output.Free;
    end;
  finally
    Doc.Free;
  end;
end;

Eine Lektion zum Freigabemodus: Wenn AppendToFile 0 zurückgab

Eine Regression in diesem Bereich ist erwähnenswert, da sich das Fehlermuster verallgemeinern lässt. AppendToFile(FileName) hängt eine inkrementelle Aktualisierung direkt an ein bestehendes PDF auf der Festplatte an — der natürliche Aufruf für einen In-Place-Audit-Trail-Workflow: eine Datei laden, eine Änderung vornehmen, an denselben Pfad anhängen. In v3.71.2 begann genau diese Abfolge, 0 zurückzugeben. Die Ursache lag im Loader, nicht im Writer: Um das Lesen großer Dokumente bei Bedarf zu unterstützen, hält LoadFromFile das Handle der Quelldatei für die Lebensdauer des Dokumentobjekts offen, und dieses Handle wurde mit fmShareDenyWrite geöffnet. Als AppendToFile dann versuchte, dieselbe Datei erneut zum Schreiben zu öffnen, verweigerte der eigene Freigabemodus des Loaders dies, und die API schlug fehl, noch bevor ein Byte geschrieben wurde

Die Korrektur lockerte den Freigabemodus des Loaders auf fmShareDenyNone, was genau deshalb sicher ist, weil eine inkrementelle Aktualisierung Folgendes tut: Sie fügt Bytes streng nach dem Ende der Datei hinzu und schreibt niemals den Bereich um, den das langlebige Handle des Lesers bedient. Die allgemeine Lektion für jeden, der diese Bibliothek kapselt — oder ähnliche Streaming-Loader baut — ist, dass lazy, Handle-haltende Reader und Writer derselben Datei in einem Spannungsverhältnis stehen und der beim Öffnen gewählte Freigabemodus ein API-Vertrag ist, kein Implementierungsdetail. Wenn AppendToFile in Ihrem Code jemals 0 zurückgibt, prüfen Sie zuerst, ob ein anderer Prozess in Ihrem System die Zieldatei noch mit einem restriktiven Freigabemodus hält

Die ehrlichen Kosten: Wann inkrementelle Aktualisierungen das falsche Werkzeug sind

Inkrementelle Aktualisierungen tauschen die Dateigröße gegen die Schreibeffizienz ein, und dieser Tausch ist nicht immer vorteilhaft. Jede Revision hängt ihre geänderten Objekte an, während die ersetzten Definitionen in der Datei verbleiben, sodass sich in einem Dokument, das hunderte Male bearbeitet wurde, tote Objekte und eine lange /Prev-Kette ansammeln, die jeder Reader durchlaufen muss. Schlimmer noch: „Gelöschter“ Inhalt ist nicht weg. Text, der in Revision fünf entfernt wurde, ist in den Bytes von Revision vier physikalisch immer noch vorhanden und kann von jedem wiederhergestellt werden, der die Datei abschneidet. Das Schwärzen, Bereinigen oder jegliche Entfernung sensibler Inhalte erfordert daher ein vollständiges Umschreiben — ein inkrementelles Speichern einer Schwärzung ist ein Datenleck mit zusätzlichen Schritten

Ein vollständiges Speichern ist auch die richtige Wahl, wenn das Ziel die Komprimierung ist (das Herausfiltern angesammelter Inkremente und ungenutzter Objekte), wenn dokumentübergreifende Eigenschaften wie die Verschlüsselung geändert werden — das erneute Verschlüsseln betrifft jede Zeichenfolge und jeden Stream, sodass an der Änderung nichts „Inkrementelles“ mehr übrig bleibt — oder wenn ein sauberes Produkt erstellt werden soll, bei dem der Bearbeitungsverlauf nicht mit der Datei transportiert werden soll. Eine vernünftige Regel: Verwenden Sie AppendToStream oder AppendToFile, solange ein Dokument aktiv ist und sich ändert, insbesondere sobald es Signaturen trägt; verwenden Sie ein vollständiges Umschreiben mit SaveToStream an Lebenszyklusgrenzen, wenn das Dokument Ihr System verlässt oder sein Verlauf flachgelegt werden muss

Inkrementelle Aktualisierungen, Delta-Ausgaben mit virtuellen Offsets und Direkt-in-Stream-Serialisierung sind alle Teil der standardmäßigen losLab PDF Library für Delphi, C# und VB.NET. Die Produktseite listet die gesamte Speicher- und Append-API-Oberfläche neben den oben besprochenen Signatur- und Großdatei-Funktionen auf