HotPDF Delphi Component kann Text in einem bestehenden PDF aus Delphi und C++Builder heraus suchen und ersetzen. SearchLoadedPageText und SearchLoadedDocumentText finden jedes Vorkommen einer Zeichenkette mit Präzision auf Glyphenebene, und ReplaceLoadedPageText sowie ReplaceLoadedDocumentText schreiben die gefundenen Bytes an Ort und Stelle um — vorausgesetzt, jedes Ersatzzeichen lässt sich über die ursprüngliche Schrift neu codieren, eine physische Einschränkung, die dieser Artikel ehrlich behandelt, statt sie in einer Fußnote zu verstecken
Der Anlass hinter dieser Funktion ist immer banal. Ein Unternehmen benennt sich um, und dreitausend archivierte Rechnungen tragen noch den alten Namen. Eine Vertragsvorlage wurde mit dem Ablaufdatum des Vorjahres ausgeliefert. Ein Produktcode wurde eingestellt, und jedes Datenblatt, das ihn erwähnt, braucht stattdessen den Nachfolgecode. In einer Textverarbeitung ist jedes davon eine Sache von dreißig Sekunden. In einem PDF ist es ein wirklich schwieriges Problem, und zu verstehen, warum, macht den Unterschied zwischen der guten Nutzung der API und einem Fehlerbericht, der in Wirklichkeit ein Zitat aus der Spezifikation ist
Warum ist das Ersetzen von Text in einem PDF so schwierig?
Das Ersetzen von Text in einem PDF ist schwierig, weil eine PDF-Seite keinen bearbeitbaren Text enthält — sie enthält positionierte Glyphen. Im Textdarstellungsmodell von ISO 32000-1 §9.4 steuert ein Content-Stream Operatoren wie Tj und TJ, die Folgen von Zeichencodes an Koordinaten malen, die die Textmatrix festlegt. Diese Codes sind kein Unicode; sie sind Indizes in das Encoding, das die Schrift der Seite deklariert, und die Rückabbildung auf lesbare Zeichen kann in einer /ToUnicode-CMap, einem Encoding-Differences-Array oder einer CID-Zuordnungskette liegen. Es gibt kein Absatzobjekt, keinen Textfluss und keine Garantie, dass ein sichtbares Wort überhaupt als eine Zeichenkette gespeichert ist
Das Ersetzen fügt über der Decodierung eine zweite Schwierigkeitsebene hinzu: Sie müssen genau wissen, welche Bytes des ursprünglichen Streams jede Glyphe erzeugt haben, damit Sie neue Bytes in exakt diesen Bereich und sonst nirgendwohin einfügen können. Ein Textextraktor kann es sich leisten, die Bytepositionen zu verwerfen, sobald er den Unicode hat. Ein Ersetzer kann das nicht. Deshalb hat HotPDF die Arbeit auf zwei Releases verteilt — v2.251.0 baute die Offset-Verfolgung und die Suchschicht, und v2.252.0 baute darauf die Umschreibschicht
Text finden: Suche auf Glyphenebene mit Byte-Offset-Verfolgung
SearchLoadedDocumentText von HotPDF findet jedes Vorkommen eines Suchbegriffs durch Abgleich mit der decodierten Unicode-Glyphenfolge jeder Seite, nicht mit rohen Stream-Bytes, sodass ein Treffer ein Treffer ist, unabhängig davon, wie die Schrift ihn codiert hat. Die Infrastruktur darunter wurde in v2.251.0 eingeführt: Der Content-Stream-Tokenizer zeichnet für jeden String-Operanden einen StartOfs/EndOfs-Bytebereich auf — einschließlich seiner Begrenzer ( ) oder < > —, und jede decodierte Glyphe trägt ein Tripel TokenIndex/ItemIndex/ByteOffset, das auf den exakten Operanden, das TJ-Array-Element und die Codeeinheit zurückzeigt, die sie erzeugt haben. Derselbe Glyphen-Interpreter treibt die Extraktions-API an, die in Text aus einem geladenen PDF in Delphi extrahieren beschrieben wird; die Suche behält lediglich die Herkunft, die die Extraktion verwirft
Jeder Treffer kommt als THPDFTextMatch-Record zurück, der den Seitenindex, den inklusiven Glyphenbereich, den X/Y-Ursprung und die Breite des Treffers im Benutzerraum, den Quelltoken- und Elementindex sowie den gefundenen Text selbst trägt. Das reicht, um ein Hervorhebungs-Overlay, eine Prüfoberfläche oder den Ersetzungsschritt zu steuern. Eine Suche, die nichts findet, gibt ein leeres Array zurück, statt zu scheitern, sodass das Aufrufmuster einfach bleibt
var
Pdf: THotPDF;
Matches: THPDFTextMatchArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoices-2025.pdf') > 0 then
begin
if Pdf.SearchLoadedDocumentText('Acme Corp', False, Matches) then
for I := 0 to Length(Matches) - 1 do
WriteLn(Format('page %d at (%.1f, %.1f): "%s"',
[Matches[I].PageIndex, Matches[I].X, Matches[I].Y,
Matches[I].Text]));
end;
finally
Pdf.Free;
end;
end;
Eine bewusste Designentscheidung verdient eine Anmerkung. Wenn CaseSensitive gleich False ist, faltet der Vergleich die Groß-/Kleinschreibung absichtlich nur für ASCII-Zeichen: Vollständiges Unicode-Case-Folding verhält sich über die von HotPDF unterstützten Toolchains von Delphi 5 bis XE unterschiedlich, und eine Such-API, die je nach dem Compiler, der Ihre Anwendung gebaut hat, andere Treffer findet, ist schlechter als eine mit einer dokumentierten, vorhersehbaren Grenze. Für lateinischen Geschäftstext — Namen, Codes, Daten — deckt ASCII-Folding die praktischen Fälle ab
Text ersetzen: umgekehrte Codierung und chirurgisches Einfügen
ReplaceLoadedDocumentText, hinzugefügt in HotPDF v2.252.0, schreibt jedes Vorkommen eines Suchbegriffs um, indem es die Decodierungsmechanik rückwärts laufen lässt. Die Funktion HPDFEncodeUnicode ist die Umkehrung des Zeichencode-Decoders: Sie durchläuft dieselbe Strategiekette in umgekehrter Richtung — /ToUnicode-bfchar- und bfrange-Lookup, Encoding-Stream-CID-Zuordnung, Type0-Identitätszuordnungen und die vordefinierten WinAnsi- und MacRoman-Tabellen —, um jedes Ersatzzeichen wieder in die Zeichencode-Bytes zu verwandeln, die die ursprüngliche Schrift erwartet. Die neu codierten Bytes werden dann in ein wohlgeformtes String-Literal oder einen Hex-String serialisiert, wobei die Escaping-Regeln des Tokenizers gespiegelt werden, sodass ein Parse-→-Reserialisierungs-Roundtrip stabil ist
Das Einfügen selbst ist chirurgisch statt pauschal. Nur der vom Treffer abgedeckte Code-Bytebereich wird innerhalb des String-Operanden ersetzt; nicht getroffene Bytes im selben Operanden, der Leerraum zwischen Tokens und jeder umgebende Operator werden wörtlich erhalten, Byte für Byte. Das Ersetzen von bca innerhalb von abcabc ergibt a + Ersatz + bc, keinen zerstörten Operanden. Ersetzungen dürfen kürzer oder länger als der Suchbegriff sein — das Literal wird neu serialisiert und die /Length des Streams aktualisiert —, und jeder /Contents-Stream einer Seite mit mehreren Streams wird isoliert verarbeitet, sodass die Seite wohlgeformt bleibt
var
Pdf: THotPDF;
ReplaceCount: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('contract-draft.pdf') > 0 then
begin
if Pdf.ReplaceLoadedDocumentText('2025-12-31', '2026-12-31',
True, ReplaceCount) then
WriteLn(Format('%d operand rewrites performed', [ReplaceCount]));
Pdf.SaveLoadedDocument('contract-final.pdf');
end;
finally
Pdf.Free;
end;
end;
Beachten Sie, was die API nicht tut: Sie setzt die Seite nicht neu. PDF kennt keinen Umfluss, sodass ein Ersatz, der visuell breiter als das Original ist, einfach mehr horizontalen Platz einnimmt und das, was rechts davon gemalt wurde, verdrängen kann. Ersetzungen gleicher oder ähnlicher Länge — Daten, Versionszeichenketten, Teilenummern, Namenskorrekturen — sind der ideale Anwendungsfall. Pauschale Umformulierungen gehören ins Quelldokument, nicht ins PDF
Warum kann man Text nicht durch Zeichen ersetzen, die das Schrift-Subset nie enthielt?
Sie können Text nicht durch ein Zeichen ersetzen, das das eingebettete Schrift-Subset nie enthielt, weil die Bytefolge, die dieses Zeichen auswählen würde, in den Zuordnungstabellen der Schrift schlicht nicht existiert. Wenn ein PDF-Erzeuger eine Subset-Schrift einbettet, decken ihre /ToUnicode-CMap und ihre Encoding-Strukturen nur die Glyphen ab, die das ursprüngliche Dokument tatsächlich verwendet hat. HPDFEncodeUnicode kann nur eine Zuordnung umkehren, die vorhanden ist: Wenn das Dokument in dieser Schrift nie den Buchstaben E enthielt, gibt es keinen Zeichencode für E, zu dem sich umkehren ließe. Das ist eine physische Eigenschaft der Datei, keine Einschränkung einer bestimmten Bibliothek — kein Werkzeug kann eine Glyphenzuordnung herbeizaubern, die nie eingebettet wurde
HotPDF behandelt den Fehlschlag konservativ. Wenn auch nur ein einzelnes Zeichen des Ersatzes nicht neu codiert werden kann, wird dieses gesamte Vorkommen des Suchbegriffs übersprungen — keine Exception, kein teilweiser Zeichensalat, und das Vorkommen wird schlicht nicht in ReplaceCount gezählt. Die praktische Konsequenz: Prüfen Sie ReplaceCount gegen die Trefferanzahl einer vorherigen Suche und betrachten Sie ein Defizit als Signal. Im obigen Datumsbeispiel muss die Ziffer 6 irgendwo im Text des Dokuments in derselben Schrift vorkommen, damit das Umschreiben gelingt — in einer Rechnung wahrscheinlich, im Allgemeinen nie garantiert. Wenn die benötigten Zeichen schlicht nicht verfügbar sind und das Ziel darin besteht, sensiblen Text zu entfernen statt umzuformulieren, ist echte Inhaltsentfernung ohnehin das bessere Werkzeug; siehe geladene PDFs in Delphi schwärzen und umstrukturieren für diesen Weg
var
Matches: THPDFTextMatchArray;
Expected, Replaced: Integer;
begin
Pdf.SearchLoadedDocumentText('Acme Corp', True, Matches);
Expected := Length(Matches);
Pdf.ReplaceLoadedDocumentText('Acme Corp', 'Apex Corp', True, Replaced);
if Replaced < Expected then
WriteLn(Format('%d occurrence(s) skipped: characters missing ' +
'from the font subset, or match spans multiple operands',
[Expected - Replaced]));
end;
Die zweite Überspringbedingung in dieser Meldung ist die andere dokumentierte Grenze: Ein Suchbegriff, der sich über mehrere String-Operanden erstreckt — etwa Hello, aufgeteilt auf [(He)(llo)] TJ-Elemente —, wird von der Suche gefunden, weil die Suche die decodierte Glyphenfolge abgleicht, aber vom Ersetzen übersprungen, weil das Umschreiben über Operandengrenzen hinweg das Zusammenführen benachbarter Bytebereiche erfordern würde. Suchen-dann-prüfen macht beide Grenzen sichtbar statt still
Was ändert sich beim Speichern in der Datei?
Ein ersetzter /Contents-Stream wird unkomprimiert gespeichert. FlateDecode-komprimierte Streams werden zur Bearbeitung dekomprimiert, und wenn HotPDF die neu aufgebauten Bytes schreibt, entfernt es den /Filter-Eintrag des Streams und aktualisiert /Length, statt neu zu komprimieren. Das resultierende PDF ist vollständig gültig und wird in gängigen Viewern normal dargestellt; der Kompromiss ist eine größere Datei für jeden bearbeiteten Stream. Für eine Stapel-Pipeline, die Tausende Dokumente verarbeitet, planen Sie dieses Wachstum ein oder führen Sie nachgelagert einen separaten Kompressionsdurchlauf aus. Wie umgeschriebene Objekte beim Speichern mit der Querverweisstruktur des Dokuments zusammenspielen, ist ein eigenes Thema, das in Objekt-Streams und inkrementelle Updates in HotPDF behandelt wird
Alles andere an der Datei bleibt unangetastet. Unberührte Streams behalten ihre Kompression, Schriften und Bilder werden nicht neu geschrieben, und das Einfügen auf Operandenebene bedeutet, dass selbst die bearbeiteten Streams sich vom Original nur dort unterscheiden, wo ein Treffer gelandet ist. Dieser Konservatismus ist gewollt: Je mehr von einem geladenen Dokument eine Bibliothek umschreibt, desto mehr Gelegenheiten hat sie, eine Eigenheit des Erzeugers zu brechen, die sie nicht vorhergesehen hat
Textsuche und -ersetzung reiht sich neben Extraktion, Schwärzung und Seitenrendering in das Werkzeugset von HotPDF für geladene Dokumente ein, alle vom selben Content-Stream-Interpreter angetrieben und von Delphi 5 bis zu den aktuellen RAD-Studio-Releases ohne externe Abhängigkeiten verfügbar. Die vollständige API-Referenz und der Test-Download finden sich auf der Produktseite der HotPDF Delphi Component