Technischer Artikel

Wiederverwendung einer THotPDF-Instanz über Dokumente hinweg in Delphi

Die Fehlermeldung lautet Bitte laden Sie das Dokument vor der Verwendung von BeginDoc (Please load the document before using BeginDoc) und sie tritt fast immer beim zweiten Durchgang auf. Das erste Dokument wird problemlos geschrieben. Wenn dieselbe THotPDF-Instanz dann aufgefordert wird, ein zweites zu beginnen, wird bei BeginDoc dieser Fehler ausgelöst, und die Meldung verweist auf das Laden eines Dokuments. Das ist genau das Gegenteil von dem, was der Code zu tun versucht. Die Diskrepanz zwischen Symptom und Meldung macht diesen Fehler so hartnäckig. Das eigentliche Problem betrifft den Lebenszyklus der Komponente; hat man das erst einmal verstanden, ist der Fehler kein Mysterium mehr

Lebenszyklus des THotPDF-Dokuments mit Create, BeginDoc, EndDoc und Free pro Ausgabedatei
Eine THotPDF-Instanz entspricht einem Dokument: Create, BeginDoc, Draw, EndDoc, Free.

Eine THotPDF-Instanz ist ein Dokument, keine Dokumentenfabrik

Man neigt leicht zu der Vorstellung, THotPDF sei ein Service-Objekt, das man einmal startet und mit Dokumenten füttert, ähnlich wie man eine Datenbankverbindung offen hält und Abfrage um Abfrage darüber laufen lässt. Das ist nicht der Fall. Eine Instanz stellt ein einzelnes Dokument dar, das gerade erstellt wird, und ihre interne Statusmaschine geht davon aus, dass sie diesen Weg nur einmal durchläuft: von leer über ein geöffnetes Dokument zu einer gespeicherten Datei. BeginDoc öffnet diesen Pfad und markiert die Instanz so, als sei ein Dokument in Bearbeitung. EndDoc serialisiert alles in FileName und schließt den Vorgang ab. Ein erneuter Aufruf von BeginDoc bei derselben beendeten Instanz fordert sie auf, wieder in einen Zustand zu wechseln, den sie nie sauber verlassen hat. Die dabei ausgelöste Schutzfunktion ist diejenige, deren Meldung zufällig das Laden erwähnt, weil intern die Bedingungen „bereit zum Starten“ und „hat ein geladenes Dokument“ gemeinsam geprüft werden

Die Meldung ist also irreführend, aber die Schutzfunktion erfüllt ihren Zweck. Sie verhindert, dass Sie ein neues Dokument über einer Komponente beginnen, die sich noch mitten im Dokument wähnt. Die Lösung besteht nicht darin, die Schutzfunktion zu umgehen, sondern darin, eine verbrauchte Instanz nicht erneut zu verwenden

Der Lebenszyklus, in der Reihenfolge, in der er stattfinden muss

Jedes Dokument, das HotPDF von Grund auf neu schreibt, folgt denselben vier Schritten, und die Reihenfolge ist nicht verhandelbar. Create weist der Komponente Speicher zu. BeginDoc öffnet das Dokument und legt die strukturellen Entscheidungen fest. Alles, was sich auf die gesamte Datei auswirkt (Seitengröße, Komprimierung, Verschlüsselung, Ausgabedateiname), muss also zwischen Create und BeginDoc eingestellt werden. Dann zeichnen Sie. Dann schreibt EndDoc die Bytes auf die Festplatte. Free gibt die Instanz frei. Zeichnungsaufrufe, die vor BeginDoc platziert werden, haben keine Seite, auf der sie landen können; Eigenschaften des gesamten Dokuments, die danach zugewiesen werden, werden stillschweigend ignoriert

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.BeginDoc;                        // opens the document
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Invoice 2026-042');
    Pdf.EndDoc;                          // writes invoice.pdf, closes it out
  finally
    Pdf.Free;                            // one instance, one document
  end;
end;

Betrachten Sie dies als die Arbeitseinheit. Ein Create, ein BeginDoc, ein EndDoc, ein Free, eine Datei auf der Festplatte. In dem Moment, in dem Sie eine zweite Datei möchten, starten Sie eine neue Arbeitseinheit, was eine neue Instanz bedeutet

Was „Wiederverwendung“ bedeuten sollte: eine neue Instanz pro Datei

Die Version, die abstürzt, versucht sparsam mit Speicherzuweisungen umzugehen: Die Komponente wird einmal erstellt, man durchläuft eine Schleife (Batch) und ruft BeginDoc und EndDoc innerhalb der Schleife auf. Der zweite Durchlauf schlägt fehl. Die funktionierende Version behandelt jede Ausgabe als ein eigenes, kurzlebiges Objekt. Die Zuweisungskosten für die Erstellung einer Komponente sind trivial im Vergleich zum Aufwand für das Layouten und Serialisieren einer PDF-Datei. Es lässt sich also nichts einsparen, indem man die Instanz aufbewahrt

procedure WriteBatch(const Names: TArray<string>);
var
  I: Integer;
  Pdf: THotPDF;
begin
  for I := 0 to High(Names) do
  begin
    Pdf := THotPDF.Create(nil);         // new instance each pass
    try
      Pdf.FileName := Names[I] + '.pdf';
      Pdf.BeginDoc;
      Pdf.CurrentPage.SetFont('Arial', [], 12);
      Pdf.CurrentPage.TextOut(50, 760, 0, 'Statement for ' + Names[I]);
      Pdf.EndDoc;
    finally
      Pdf.Free;
    end;
  end;
end;

Der in der Schleife befindliche try/finally-Block ist es wert, bei der Überprüfung beibehalten zu werden. Wenn BeginDoc oder ein beliebiger Zeichnungsaufruf mitten in einem Dokument abbricht, wird die Instanz dieses Durchlaufs immer noch freigegeben, bevor der nächste beginnt. So führt ein fehlerhafter Datensatz nicht zu einer halb erstellten Komponente und ruiniert den Rest des Durchlaufs. Zieht man Create vor die Schleife, um zu „optimieren“, hat man wieder denselben Fehler, nur dass er jetzt in einer Batchschleife steckt

Das Ändern einer vorhandenen Datei ist ein anderer Einstiegspunkt

Es gibt eine zweite, völlig legitime Lesart der „Wiederverwendung“: Sie möchten kein leeres Dokument, sondern eine bereits vorhandene PDF-Datei öffnen und diese ändern. Dieser Weg geht überhaupt nicht über BeginDoc. Das ist genau der Grund, warum in der Fehlermeldung vom Laden gesprochen wird. Sie laden die Datei, bearbeiten sie und speichern sie unter dem gewünschten Namen

var
  Pdf: THotPDF;
  PageCount: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    PageCount := Pdf.LoadFromFile('contract.pdf');
    if PageCount > 0 then
    begin
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
      Pdf.CurrentPage.TextOut(40, 30, 0, 'REVIEWED');
      Pdf.SaveLoadedDocument('contract-reviewed.pdf');
    end;
  finally
    Pdf.Free;
  end;
end;

LoadFromFile gibt die Anzahl der Seiten zurück, und ein Wert von null oder darunter bedeutet, dass das Laden fehlgeschlagen ist. Es lohnt sich also, dies zu überprüfen, bevor Sie CurrentPage aufrufen. Auf die Kombination kommt es an: Ein Dokument, das Sie mit LoadFromFile geöffnet haben, wird mit SaveLoadedDocument gespeichert, nicht mit dem Paar BeginDoc/EndDoc, das zu Dokumenten gehört, die Sie aus dem Nichts erstellen. Das Vermischen der beiden Methoden ist der häufigste Weg, die gleiche Statusmaschine zu verwirren, die den ursprünglichen Fehler hervorgerufen hat. Halten Sie die beiden Abläufe gedanklich getrennt: BeginDoc ... EndDoc erstellt, LoadFromFile ... SaveLoadedDocument bearbeitet

Das Dateisperrproblem ist real, und die Lösung besteht nicht darin, Viewer-Fenster zu beenden

Der Wiederverwendungsfehler tritt oft zusammen mit einer zweiten Beschwerde auf, und die beiden verwickeln sich miteinander, da sie im selben Arbeitsablauf zur erneuten Generierung der Datei auftreten. Ein Benutzer öffnet das PDF, das Sie gerade erstellt haben, lässt es in Acrobat oder Foxit geöffnet und löst dann einen Neuaufbau aus. EndDoc versucht, denselben Pfad zu schreiben. Das Betriebssystem weigert sich, da der Viewer eine Lesefreigabe besitzt, die Schreibzugriffe blockiert, und Sie erhalten einen Fehler wegen verweigertem Zugriff. Hierbei handelt es sich tatsächlich um ein Windows-Dateisperrproblem und nicht um ein Problem mit dem Komponentenstatus, und es verdient eine echte Antwort statt eines Workarounds

Der Workaround, der im Umlauf ist – das Auflisten der Fenster der obersten Ebene und das Senden von WM_CLOSE an alles, dessen Titel wie ein PDF-Viewer aussieht – ist der falsche Instinkt. Er greift über Prozessgrenzen hinweg ein, um Fenster zu schließen, die Ihr Programm nicht besitzt, rät Viewer anhand des Titeltextes und kann ungespeicherte Anmerkungen eines Benutzers ungefragt verwerfen. Betrachten Sie diesen gesamten Ansatz als schlecht. Die zuverlässige Lösung besteht darin, niemals in einen Pfad zu schreiben, der möglicherweise von einem anderen Prozess blockiert wird. Serialisieren Sie in eine temporäre Datei im selben Verzeichnis und tauschen Sie sie dann durch atomares Umbenennen aus, sobald EndDoc erfolgreich ist. Wenn ein Viewer die alte Datei noch geöffnet hat, wird die Umbenennung entweder sauber erfolgreich ausgeführt oder schlägt lautstark fehl, und Sie erhalten eine klare Meldung, anstatt gegen die Sperre ankämpfen zu müssen

Bei einem Hochleistungsserver, der Dokumente ständig neu generiert, ist es eine sauberere Methode, jede Ausgabe unter einem eindeutigen Namen (z. B. einem Zeitstempel oder einer Job-ID) zu schreiben. So konkurrieren nie zwei Durchläufe um denselben Pfad. Alte Dateien können dann über eine separate Aufbewahrungsrichtlinie bereinigt werden. Das Prinzip ist bei beiden Ansätzen identisch: Konzipieren Sie den Vorgang so, dass die zu schreibende Datei in dem Moment, in dem Sie sie schreiben, allein Ihnen gehört. Die Sperre verschwindet nicht deshalb, weil Sie das Schließen eines Fensters erzwungen haben, sondern weil kein anderer Prozess auf die Bytes zugreift

Die Art der Problembehebung

Betrachtet man die beiden Probleme genauer, so geht es bei beiden um die Einhaltung von Grenzen. Beim Zustandsmaschinen-Fehler sollen Sie die Instanzgrenze berücksichtigen: ein THotPDF, ein Dokument. Dann gibt man es frei und erstellt ein neues. Beim Dateisperrfehler sollen Sie die Dateigrenze einhalten: Schreiben Sie dort, wo nichts anderes liest, und verschieben Sie das Ergebnis dann an die richtige Stelle. Beide Fehler erfordern weder das Patchen der Bibliothek noch Skripting auf dem Desktop. Beide laufen darauf hinaus, jedes Dokument als geschlossene Arbeitseinheit zu behandeln, die neu erstellt, sauber geschrieben und wieder freigegeben wird. Das ist dasselbe Muster, das den Rest der Komponente berechenbar macht

Die hier gezeigten Aufrufe BeginDoc, EndDoc, LoadFromFile und SaveLoadedDocument sind Teil der HotPDF-Komponente für Delphi und C++Builder