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
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 TPdf — Text, 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 already closed the cached text page, so this FindFirst
// call rebuilds it fresh before it searches
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, cached in FTextPage
Pdf.SetText(0, 'Amended Clause 4.2');
// SetText already closed RawHandle and set Pdf.TextPage back to nil.
// Calling any FPDFText_* function against the old value now touches a
// handle PDFium has already freed — undefined behavior, not a bug you
// can catch with a nil check
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
var
Pdf: TPdf;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'watermarked.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
// Strip every text object that looks like a draft watermark. Each
// RemoveObject call already invalidates the cache on its own, so
// nothing needs refreshing by hand between iterations
for I := Pdf.ObjectCount - 1 downto 0 do
if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
Pdf.RemoveObject(I, True);
// Query once, after the whole batch is done, not once per removal
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
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