Technischer Artikel

Veralteter Text nach dem Bearbeiten: PDFiums FPDF_TEXTPAGE-Cache

Sie rufen AddText auf, um mit PDFiumPas eine Zeile auf eine PDF-Seite zu stempeln, dann sofort FindFirst, um zu bestätigen, dass der Stempel gelandet ist, und die Suche kommt leer zurück. Der Text ist auf der Seite — Acrobat zeigt ihn —, aber PDFiumPas' TPdf-Komponente hält eine separate zwischengespeicherte FPDF_TEXTPAGE-Struktur, einmal aus dem Content-Stream der Seite geparst, und eine Bearbeitung aktualisiert diese Struktur nicht von selbst rückwirkend. Fragen Sie sie ab, bevor sie aufgefrischt wurde, lesen Sie die Seite genau so, wie sie vor Ihrer Änderung aussah, nicht danach

Warum gibt PDFium direkt nach einer Bearbeitung veralteten Text zurück?

PDFiumPas umschließt Googles PDFium-Render-Engine für Delphi und C++Builder, und seine Text- und Bearbeitungsaufrufe erreichen zwei unterschiedliche Subsysteme innerhalb dieser Engine. FPDF_TEXTPAGE gehört zur Leseseite: FPDFText_LoadPage durchläuft den Content-Stream der Seite einmal und baut die Textseite auf — Zeichencodes, Positionen, Font-Metriken, Wortgrenzen —, und PDFiumPas hält diese Struktur zwischengespeichert, solange die Seite geladen bleibt. Bearbeitungsaufrufe wie FPDFPage_InsertObject oder FPDFPage_GenerateContent operieren auf einer völlig anderen Repräsentation, dem Objekt- und Content-Stream-Graphen der Seite, und PDFium schiebt diese Änderungen nicht von sich aus in eine bereits geöffnete Textseite. Sie bei jeder Bearbeitung neu aufzubauen würde Batch-Bearbeitung inakzeptabel langsam machen, sodass das Design diese Kosten stattdessen gegen eine Regel eintauscht — wer auch immer das Handle hält, schließt es nach einer inhaltsändernden Bearbeitung, und die nächste Lesung baut ein frisches auf

PDFium-Bearbeitungen schreiben in den Seitencontent-Stream, während das gecachte FPDF_TEXTPAGE ein Ladezeit-Schnappschuss bleibt; eine Delphi-FindFirst-Abfrage direkt nach AddText liest die Vorbearbeitungs-Seite und verfehlt den Stempel
Bearbeiten und Lesen sind zwei getrennte Subsysteme in PDFium; die gecachte Textseite ist ein Snapshot aus der Ladezeit, und keine Bearbeitung frischt sie von selbst auf

Innerhalb von TPdfs Text-Cache: FTextPage, LoadTextPage und UnloadTextPage

TPdf verfolgt das zwischengespeicherte Handle in einem einzigen privaten Feld, FTextPage, und umschließt seinen Lebenszyklus mit zwei Methoden. LoadTextPage prüft, ob FTextPage nil ist, und ruft nur in diesem Fall FPDFText_LoadPage gegen die aktuelle Seite auf; existiert bereits ein Handle, verwendet LoadTextPage es wieder, ohne zu fragen, ob sich die Seite seit seinem Aufbau geändert hat. UnloadTextPage ist die andere Hälfte: Sie schließt das native Handle mit FPDFText_ClosePage, setzt FTextPage zurück auf nil und verwirft auch die zwischengespeicherte Web-Link-Liste sowie jede laufende Find-Sitzung, da beide aus derselben Textseite abgeleitet waren und aus demselben Grund veralten

LoadTextPages Wiederverwendung-ohne-Prüfung-Verhalten ist genau der Grund, warum die Reihenfolge zählt. Jede Textabfrage auf TPdfText, FindFirst, GetWebLinks — läuft zuerst durch LoadTextPage, sodass, solange FTextPage noch das Vor-Bearbeitungs-Handle hält, keiner dieser Aufrufe irgendeine Möglichkeit hat zu wissen, dass eine Änderung passiert ist. Seitennavigation war hier nie das Risiko: UnloadPage, das bei Seitenwechseln, Neu-Laden und Dokumentschließen läuft, hat die Textseite zusammen mit der Seite selbst immer geschlossen. Die offene Frage betraf immer Bearbeitungen, die auf die Seite angewendet werden, auf der Sie noch sitzen

Welche PDFiumPas-Methoden aktualisieren den Cache automatisch?

TPdfs eigene Seitenbearbeitungsmethoden — AddText, SetText, SetTextPositions, AddPath, RemoveObject und InsertFormObjectFromXObject — rufen jeweils UnloadTextPage auf, bevor sie UpdatePage (PDFiums FPDFPage_GenerateContent) aufrufen, um die Änderung in den Content-Stream zu serialisieren. Rufen Sie eine davon auf, baut der allernächste Text-, FindFirst- oder GetWebLinks-Aufruf die Textseite aus dem Inhalt neu auf, so wie er jetzt steht, ohne dass Ihrerseits ein zusätzlicher Aufruf nötig wäre

var
  Pdf: TPdf;
  Index: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'invoice.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
    // AddText hat die gecachte Textseite bereits geschlossen, daher baut
    // dieser FindFirst-Aufruf sie vor der Suche frisch auf
    Index := Pdf.FindFirst('Reviewed by J. Alvarez');
    if Index >= 0 then
      ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
  finally
    Pdf.Free;
  end;
end;

Das Muster, das trotzdem noch bricht: das rohe TextPage-Handle zwischenspeichern

TPdf legt das lebende Handle über eine schreibgeschützte TextPage-Eigenschaft frei, für den seltenen Fall, dass Sie eine FPDFText_*-Funktion aufrufen müssen, die PDFiumPas nicht umschlossen hat. Diese Fluchtluke ist auch der eine Ort, an dem die automatische Invalidierung nicht helfen kann: Sobald Sie den FPDF_TEXTPAGE-Wert aus der Eigenschaft in eine lokale Variable kopieren, hat PDFiumPas keine Möglichkeit zu wissen, dass Sie ihn noch halten, und keine Möglichkeit, Ihre Kopie zu aktualisieren, wenn UnloadTextPage irgendwo anders in Ihrem Code läuft

var
  Pdf: TPdf;
  RawHandle: FPDF_TEXTPAGE;
  StaleCount: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'contract.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    RawHandle := Pdf.TextPage;    // FPDFText_LoadPage-Handle, in FTextPage zwischengespeichert
    Pdf.SetText(0, 'Amended Clause 4.2');
    // SetText hat RawHandle bereits geschlossen und Pdf.TextPage auf nil zurückgesetzt.
    // Jeder FPDFText_*-Aufruf gegen den alten Wert berührt nun ein
    // Handle, das PDFium bereits freigegeben hat — undefiniertes Verhalten, kein Fehler, den
    // man mit einem nil-Check abfangen kann
    StaleCount := FPDFText_CountChars(RawHandle);
  finally
    Pdf.Free;
  end;
end;

Ein Handle zu verwenden, nachdem FPDFText_ClosePage darauf gelaufen ist, ist in PDFium selbst undefiniertes Verhalten, keine PDFiumPas-Konvention, die man ignorieren könnte — es kann die zuletzt bekannten Daten zurückgeben, nichts zurückgeben oder den Prozess zum Absturz bringen, und welches davon bei einem gegebenen Build passiert, ist nichts, worauf sich Anwendungscode verlassen sollte. Die sichere Regel ist eng gefasst: Pdf.TextPage frisch lesen, unmittelbar vor dem FPDFText_*-Aufruf, der es braucht, und nie eine Kopie über eine Anweisung hinweg halten, die die Seite bearbeiten könnte

Bearbeitungen bündeln, dann einmal abfragen

Nichts davon bedeutet, dass jeder AddText- oder RemoveObject-Aufruf direkt danach eine defensive Textabfrage braucht, um das Ergebnis zu prüfen. Jede Bearbeitungsmethode bezahlt bereits einmal die Kosten, die Textseite zu schließen; nach jeder einzelnen Bearbeitung innerhalb einer Schleife abzufragen bezahlt diese Kosten ohne Nutzen erneut, da FPDFText_LoadPage bei jedem Lauf den gesamten Content-Stream erneut durchläuft

TPdf-Bearbeitungsmethoden wie AddText, SetText und RemoveObject rufen UnloadTextPage vor UpdatePage auf, sodass die nächste Delphi-Text-, FindFirst- oder GetWebLinks-Abfrage FPDF_TEXTPAGE aus dem bearbeiteten Inhalt neu aufbaut
Jede verpackte Bearbeitung wirft zuerst die veraltete Textseite weg und erzeugt zweitens Inhalt; die nächste Textabfrage baut dann FPDF_TEXTPAGE automatisch neu
var
  Pdf: TPdf;
  I: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'watermarked.pdf';
    Pdf.Active := True;
    Pdf.PageNumber := 1;

    // Jedes Textobjekt entfernen, das wie ein Entwurfswasserzeichen aussieht. Jeder
    // RemoveObject-Aufruf invalidiert den Cache bereits eigenständig, sodass
    // zwischen den Iterationen nichts von Hand aktualisiert werden muss
    for I := Pdf.ObjectCount - 1 downto 0 do
      if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
        Pdf.RemoveObject(I, True);

    // Einmal abfragen, nachdem die gesamte Stapelverarbeitung abgeschlossen ist, nicht pro Entfernung
    if Pdf.FindFirst('DRAFT') < 0 then
      ShowMessage('Watermark cleared');
  finally
    Pdf.Free;
  end;
end;

Dieselbe Bündelungslogik gilt speziell für den Suchzustand. FindNext und FindPrevious setzen eine von FindFirst gestartete Sitzung fort, und diese Sitzung wird zusammen mit allem anderen von UnloadTextPage abgebaut, sodass ein erneuter Aufruf von FindNext nach einer Bearbeitung — statt erneut FindFirst aufzurufen — eine Exception auslöst, statt still eine Suche gegen Inhalt fortzusetzen, der nicht mehr existiert. Behandeln Sie jede Bearbeitung als harte Grenze sowohl für Textinhalt als auch für Suchposition, und lassen Sie ein frisches FindFirst auf der anderen Seite Ihrer Bearbeitungen die Suche wieder aufnehmen

Das rohe FPDF_TEXTPAGE-Handle aus der TPdf-TextPage-Eigenschaft kopieren und nach SetText FPDFText_CountChars darauf aufrufen lässt Delphi-Code ein bereits freigegebenes PDFium-Handle nutzen - undefiniertes Verhalten
Ein kopierter FPDF_TEXTPAGE-Wert zeigt weiter auf ein Handle, das der Bearbeitungspfad bereits geschlossen hat; lesen Sie Pdf.TextPage stattdessen frisch unmittelbar vor jedem unverpackten FPDFText_*-Aufruf

Wo das zur Extraktions- und Annotationsarbeit passt

Reine Textextraktion — den Text einer Seite lesen, ohne etwas zu ändern — läuft nie in irgendetwas davon hinein, weil nichts ein Handle invalidiert, das keine Bearbeitung berührt hat. Wie Text, Zeichenrechtecke und Wortgrenzen auf einer unveränderten Seite funktionieren, behandelt der begleitende Artikel zur Textextraktion mit PDFiumPas, ohne den hier zusätzlich behandelten Textseiten-Cache-Lebenszyklus

Der Cache-Lebenszyklus zählt am meisten in Workflows, die bearbeiten und dann sofort auf das Ergebnis reagieren: eine Korrektur stempeln und danach suchen, einen Absatz schwärzen und bestätigen, dass er weg ist, oder eine Phrase lokalisieren, um eine Markup-Annotation direkt nach dem Einfügen von Text daneben zu verankern. Dieser letzte Fall lohnt es sich, eigens hervorzuheben — Quad-Point-Markup-Annotationen werden aus Zeichenrechtecken positioniert, die von der Textseite gelesen werden, sodass eine aus vor einer Bearbeitung erfassten Koordinaten gebaute Annotation am Ende die falsche Stelle hervorhebt, sobald die Bearbeitung landet

TPdfs Bearbeitungs- und Text-APIs sind Teil der PDFium-Komponente für Delphi und C++Builder, und die Produktseite trägt die vollständige Methodenreferenz für die hier behandelten Bearbeitungs-, Extraktions- und Suchoberflächen