Die HotPDF-Komponente kann Text in einem bestehenden PDF aus Delphi und C++Builder suchen und ersetzen. SearchLoadedPageText und SearchLoadedDocumentText lokalisieren jedes Vorkommen einer Zeichenfolge mit einer Genauigkeit auf Glyphen-Ebene, und ReplaceLoadedPageText und ReplaceLoadedDocumentText schreiben die übereinstimmenden Bytes direkt an Ort und Stelle um — vorausgesetzt, jedes Ersatzzeichen kann über die ursprüngliche Schriftart neu codiert werden, eine physische Einschränkung, die in diesem Artikel offen behandelt und nicht in einer Fußnote versteckt wird
Die Anfrage hinter dieser Funktion ist immer alltäglich. Ein Unternehmen benennt sich um und dreitausend archivierte Rechnungen tragen immer noch den alten Namen. Eine Vertragsvorlage wurde mit dem Ablaufdatum des letzten Jahres versendet. Eine Produktnummer wurde eingestellt und jedes Datenblatt, das sie erwähnt, benötigt stattdessen die Nachfolgenummer. In einer Textverarbeitung ist jede dieser Aufgaben in dreißig Sekunden erledigt. In einem PDF handelt es sich um ein wirklich schwieriges Problem, und das Verständnis des „Warum“ macht den Unterschied aus zwischen der erfolgreichen Nutzung der API und dem Einreichen eines Fehlerberichts, der eigentlich ein Zitat der Spezifikation ist
Warum ist das Ersetzen von Text in einer PDF-Datei so schwierig?
Das Ersetzen von Text in einem PDF ist schwierig, weil eine PDF-Seite keinen editierbaren Text enthält — sie enthält positionierte Glyphen. Gemäß dem Textanzeigemodell von ISO 32000-1 §9.4 steuert ein Inhaltsstrom Operatoren wie Tj und TJ, die Sequenzen von Zeichencodes an Koordinaten zeichnen, die durch die Textmatrix festgelegt wurden. Diese Codes sind kein Unicode. Es sind Indizes in das Encoding, das die Schriftart der Seite deklariert, und die Rückabbildung auf lesbare Zeichen kann in einer /ToUnicode-CMap, einem Encoding-Difference-Array oder einer CID-Mapping-Kette liegen. Es gibt kein Absatzobjekt, keinen Textfluss und keine Garantie dafür, dass ein visuelles Wort überhaupt als eine zusammenhängende Zeichenfolge gespeichert wird
Das Ersetzen fügt dem Dekodieren eine zweite Schwierigkeitsebene hinzu: Sie müssen genau wissen, welche Bytes des ursprünglichen Stroms jede Glyphe erzeugt haben, damit Sie neue Bytes präzise in diesen Abschnitt einfügen können und in nichts anderes. Ein Textextraktor kann es sich leisten, die Bytepositionen zu verwerfen, sobald er das Unicode-Zeichen ausgelesen hat. Ein Ersetzungswerkzeug kann das nicht. Aus diesem Grund hat HotPDF die Arbeit auf zwei Releases aufgeteilt — v2.251.0 implementierte das Offset-Tracking und die Suchschicht, und v2.252.0 baute die Umschreibschicht darauf auf
Text finden: Glyphen-Ebene-Suche mit Byte-Offset-Tracking
HotPDFs SearchLoadedDocumentText findet jedes Vorkommen eines Suchbegriffs durch Abgleich mit der dekodierten Unicode-Glyphenfolge jeder Seite, nicht mit den rohen Stream-Bytes. Ein Treffer ist also ein Treffer, unabhängig davon, wie die Schriftart ihn codiert hat. Die darunter liegende Infrastruktur wurde in v2.251.0 eingeführt: Der Content-Stream-Tokenizer zeichnet einen StartOfs/EndOfs-Bytebereich für jeden String-Operanden auf — einschließlich seiner Begrenzer ( ) oder < > — und jede dekodierte Glyphe trägt ein Tripel aus TokenIndex/ItemIndex/ByteOffset, das auf den genauen Operanden, das TJ-Array-Element und die Codeeinheit verweist, die sie erzeugt haben. Derselbe Glyphen-Interpreter treibt auch die Extraktions-API an, die im Artikel Extrahieren von Text aus geladenen PDFs in Delphi beschrieben wird. Die Suche behält lediglich die Herkunft bei, die bei der Extraktion verworfen wird
Jeder Treffer wird als THPDFTextMatch-Datensatz zurückgegeben, der den Seitenindex, den inklusiven Glyphenbereich, den X/Y-Ursprung im Benutzerbereich und die Breite des Treffers, den Quell-Token- und Elementindex sowie den übereinstimmenden Text selbst enthält. Das reicht aus, um eine Hervorhebung, eine Benutzeroberfläche zur Überprüfung oder den Ersetzungsschritt zu steuern. Eine Suche, die nichts findet, gibt ein leeres Array zurück, anstatt fehlzuschlagen, wodurch 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('Seite %d bei (%.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 den Wert False hat, ignoriert der Vergleich die Groß-/Kleinschreibung nur bei ASCII-Zeichen. Dies ist Absicht: Die vollständige Unicode-Groß-/Kleinschreibung verhält sich in den von HotPDF unterstützten Toolchains von Delphi 5 bis XE unterschiedlich, und eine Such-API, die je nach Compiler, der Ihre Anwendung erstellt hat, unterschiedliche Treffer findet, ist schlechter als eine mit einer dokumentierten, vorhersagbaren Einschränkung. Für lateinische Geschäftstexte — Namen, Codes, Daten — deckt das ASCII-Folding die praktischen Anwendungsfälle ab
Text ersetzen: Umgekehrte Codierung und präzises Einfügen
ReplaceLoadedDocumentText, hinzugefügt in HotPDF v2.252.0, schreibt jedes Vorkommen eines Suchbegriffs um, indem es die Dekodierungslogik rückwärts ausführt. Die Funktion HPDFEncodeUnicode ist das Gegenstück zum Zeichencode-Decoder: Sie durchläuft dieselbe Kette in umgekehrter Reihenfolge — Suche in den Tabellen /ToUnicode bfchar und bfrange, CID-Mapping des Encoding-Streams, Type0-Identity-Mappings und die vordefinierten WinAnsi- und MacRoman-Tabellen — um jedes Ersatzzeichen in die von der Originalschriftart erwarteten Zeichencode-Bytes zurückzuwandeln. Die neu codierten Bytes werden dann in ein wohlgeformtes String-Literal oder einen Hex-String serialisiert, was die Escaping-Regeln des Tokenizers widerspiegelt, sodass der Zyklus aus Parsen und Reserialisieren stabil bleibt
Das Einfügen selbst erfolgt präzise an Ort und Stelle und nicht flächendeckend. Nur der vom Treffer abgedeckte Code-Byte-Bereich wird innerhalb des String-Operanden ersetzt. Nicht übereinstimmende Bytes im selben Operanden, die Leerzeichen zwischen den Token und alle umgebenden Operatoren bleiben unverändert, Byte für Byte, erhalten. Das Ersetzen von bca innerhalb von abcabc ergibt a + Ersetzung + bc, nicht einen überschriebenen Operanden. Ersetzungen können kürzer oder länger als der Suchbegriff sein — das Literal wird reserialisiert und die /Length des Streams aktualisiert — und jeder Inhaltsstrom einer mehrseitigen Seite wird isoliert verarbeitet, damit die Seite intakt 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 bietet keinen automatischen Zeilenumbruch. Ein Ersatz, der visuell breiter ist als das Original, belegt daher mehr horizontalen Platz und kann rechts davon gezeichnete Elemente überlappen. Ersetzungen gleicher oder ähnlicher Länge — Daten, Versionsnummern, Teilenummern, Namenskorrekturen — sind der ideale Anwendungsfall. Grundlegende Textänderungen gehören in das Quelldokument, nicht in die PDF-Datei
Warum können Sie Text nicht durch Zeichen ersetzen, die das Schriftart-Subset nie enthielt?
Sie können Text nicht durch ein Zeichen ersetzen, das das eingebettete Schriftart-Subset nie enthalten hat, da die Bytefolge zur Auswahl dieses Zeichens in den Zuordnungstabellen der Schriftart schlicht nicht existiert. Wenn ein PDF-Erstellungsprogramm eine Subset-Schriftart einbettet, decken seine /ToUnicode-CMap- und Codierungsstrukturen nur die Glyphen ab, die das ursprüngliche Dokument tatsächlich verwendet hat. HPDFEncodeUnicode kann nur eine vorhandene Zuordnung umkehren: Wenn das Dokument den Buchstaben E in dieser Schriftart nie enthalten hat, gibt es keinen Zeichencode für E, auf den zurückgegriffen werden kann. Dies ist eine physische Eigenschaft der Datei und keine Einschränkung einer bestimmten Bibliothek — kein Tool kann eine Glyphenzuordnung herbeizaubern, die nie eingebettet war
HotPDF behandelt diesen Fehlerfall konservativ. Kann ein einzelnes Zeichen der Ersetzung nicht neu codiert werden, wird dieses gesamte Vorkommen des Suchbegriffs übersprungen — keine Ausnahme, kein teilweise fehlerhafter Text — und das Vorkommen wird in ReplaceCount schlicht nicht mitgezählt. Die praktische Folge: Vergleichen Sie ReplaceCount mit der Anzahl der Treffer einer vorherigen Suche und werten Sie eine Differenz als Signal. Im obigen Datumsbeispiel muss die Ziffer 6 irgendwo im Text des Dokuments in derselben Schriftart vorkommen, damit das Umschreiben gelingt — was bei einer Rechnung wahrscheinlich, im Allgemeinen aber nie garantiert ist. Wenn die benötigten Zeichen schlicht nicht verfügbar sind und das Ziel darin besteht, sensiblen Text zu entfernen, anstatt ihn umzuformulieren, ist das echte Entfernen von Inhalten ohnehin der bessere Weg. Siehe Schwärzen und Restrukturieren von geladenen PDFs in Delphi für diesen Ansatz
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 Skip-Bedingung in dieser Meldung ist die andere dokumentierte Grenze: Ein Suchbegriff, der sich über mehrere String-Operanden erstreckt — z. B. Hello aufgeteilt auf die Elemente [(He)(llo)] TJ — wird von der Suche gefunden, da die Suche die dekodierte Glyphensequenz vergleicht, wird aber beim Ersetzen übersprungen. Das Umschreiben über Operandengrenzen hinweg würde das Zusammenführen benachbarter Bytebereiche erfordern. Die Suche mit anschließender Überprüfung macht beide Einschränkungen sichtbar
Was ändert sich in der Datei beim Speichern?
Ein ersetzter /Contents-Strom wird unkomprimiert gespeichert. FlateDecode-komprimierte Streams werden zur Bearbeitung dekomprimiert, und wenn HotPDF die neu erstellten Bytes schreibt, entfernt es den Eintrag /Filter des Streams und aktualisiert die /Length, anstatt sie erneut zu komprimieren. Das resultierende PDF ist vollkommen gültig und wird in gängigen Viewern normal gerendert. Der Nachteil ist eine größere Datei für jeden bearbeiteten Stream. Für eine Stapel-Pipeline, die Tausende von Dokumenten verarbeitet, sollten Sie dieses Wachstum einplanen oder einen separaten Komprimierungsdurchlauf nachschalten. Wie umgeschriebene Objekte beim Speichern mit der Kreuzverweisstruktur des Dokuments interagieren, ist ein eigenes Thema, das unter Objektströme und inkrementelle Aktualisierungen in HotPDF behandelt wird
Alles andere in der Datei bleibt unberührt. Unbearbeitete Streams behalten ihre Komprimierung, Schriftarten und Bilder werden nicht neu geschrieben, und das präzise Einfügen auf Operandenebene sorgt dafür, dass sich selbst die bearbeiteten Streams vom Original nur dort unterscheiden, wo ein Treffer gelandet ist. Diese Zurückhaltung ist Absicht: Je mehr Teile eines geladenen Dokuments eine Bibliothek umschreibt, desto mehr Möglichkeiten hat sie, eine nicht vorhergesehene Eigenheit des Erstellungsprogramms zu beschädigen
Die Textsuche und das Ersetzen ergänzen die Extraktion, Schwärzung und das Seitenrendering im Werkzeugsatz für geladene Dokumente von HotPDF, die alle vom selben Content-Stream-Interpreter gesteuert werden und von Delphi 5 bis zu den aktuellen RAD-Studio-Versionen ohne externe Abhängigkeiten verfügbar sind. Die vollständige API-Referenz und der Test-Download befinden sich auf der Produktseite von HotPDF Component