Technisch artikel

Zoeken en vervangen van tekst in een bestaande PDF met Delphi

HotPDF Component kan tekst zoeken en vervangen in een bestaande PDF vanuit Delphi en C++Builder. SearchLoadedPageText en SearchLoadedDocumentText lokaliseren elke instantie van een string met precisie op glyph-niveau, en ReplaceLoadedPageText and ReplaceLoadedDocumentText herschrijven de overeenkomende bytes ter plaatse — mits elk vervangend teken opnieuw kan worden gecodeerd via het originele lettertype, een fysieke beperking die in dit artikel eerlijk wordt behandeld in plaats van te worden weggestopt in een voetnoot

De vraag achter deze functie is altijd alledaags. Een bedrijf verandert van naam en drieduizend gearchiveerde facturen dragen nog steeds de oude naam. Een contractsjabloon is verzonden met de vervaldatum van vorig jaar. Een product code is uitgefaseerd en elk gegevensblad dat deze vermeldt, heeft in plaats daarvan de opvolgende code nodig. In een tekstverwerker is elk van deze taken een klusje van dertig seconden. In een PDF is het een echt moeilijk probleem, en begrijpen waarom maakt het verschil tussen de API goed gebruiken en het indienen van een bugrapport dat eigenlijk een specificatie-citaat is

Waarom is het vervangen van tekst in een PDF zo moeilijk?

Tekst vervangen in een PDF is moeilijk omdat een PDF-pagina geen bewerkbare tekst bevat — deze bevat gepositioneerde glyphs. Onder het model voor tekstweergave van ISO 32000-1 §9.4 stuurt een inhoudsstream operatoren aan zoals Tj en TJ die reeksen tekencodes tekenen op coördinaten die zijn vastgesteld door de tekstmatrix. Die codes zijn geen Unicode; het zijn indices in de codering die het lettertype of de pagina declareert, en de terugkoppeling naar leesbare tekens bevindt zich mogelijk in een /ToUnicode CMap, een coderingsverschilmatrix (encoding difference array) of een CID-koppelingsketen. Er is geen paragraafobject, geen tekststroom en geen garantie dat één visueel woord zelfs maar als één string is opgeslagen

Vervanging voegt een tweede moeilijkheidsgraad toe bovenop decodering: u moet exact weten welke bytes van de originele stroom elke glyph hebben geproduceerd, zodat u nieuwe bytes in precies dat bereik kunt splitsen en niets anders. Een tekstextractor kan het zich veroorloven om de byteposities weg te gooien zodra hij de Unicode eruit heeft. Een vervanger kan dat niet. Daarom heeft HotPDF het werk over twee releases verdeeld — v2.251.0 bouwde de offset-tracking en zoeklaag, en v2.252.0 bouwde de herschrijflaag daarbovenop

Tekst zoeken: zoeken op glyph-niveau met tracking van byte-offsets

SearchLoadedDocumentText van HotPDF vindt elke instantie van een zoekterm door deze te vergelijken met de gedecodeerde Unicode-glyphvolgorde van elke pagina, niet met ruwe streambytes, dus een treffer is een treffer, ongeacht hoe het lettertype deze heeft gecodeerd. De onderliggende infrastructuur werd geïntroduceerd in v2.251.0: de inhoudsstream-tokenizer registreert een StartOfs/EndOfs bytebereik voor elke string-operand — inclusief de scheidingstekens ( ) of < > — en elke gedecodeerde glyph draagt een TokenIndex/ItemIndex/ByteOffset drietal dat terugverwijst naar de exacte operand, het TJ-array-item en de code-eenheid die deze heeft geproduceerd. Dezelfde glyph-interpreter drijft de extractie-API aan die wordt beschreven in tekst extraheren uit een geladen PDF in Delphi; zoeken behoudt simpelweg de herkomst die extractie weggooit

Elke treffer komt terug als een THPDFTextMatch-record met daarin de pagina-index, het inclusieve glyphbereik, de X/Y-oorsprong en -breedte in gebruikersruimte van de treffer, de brontoken- en item-index, en de overeenkomende tekst zelf. Dat is voldoende om een markeringsoverlay (highlight overlay), een controle-UI of de vervangingsstap aan te sturen. Een zoekopdracht die niets oplevert, retourneert een lege array in plaats van te mislukken, zodat het aanroepingspatroon eenvoudig blijft

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;

Eén bewuste ontwerpkeuze verdient een toelichting. Wanneer CaseSensitive False is, vouwt de vergelijking (case folding) alleen ASCII-tekens om, inherent aan het ontwerp: volledige Unicode-case folding gedraagt zich anders over de Delphi 5 tot en met XE toolchains die HotPDF ondersteunt, en een zoek-API die verschillende resultaten oplevert afhankelijk van welke compiler uw applicatie heeft gebouwd, is slechter dan een met een gedocumenteerde, voorspelbare limiet. Voor Latijnse zakelijke tekst — namen, codes, datums — dekt ASCII-folding de praktische gevallen

Tekst vervangen: omgekeerde codering en chirurgische splitsing

ReplaceLoadedDocumentText, toegevoegd in HotPDF v2.252.0, herschrijft elke instantie van een zoekterm door de decoderingsfunctionaliteit achterstevoren uit te voeren. De functie HPDFEncodeUnicode is the inverse van de tekencode-decoder: deze doorloopt dezelfde strategieketen in omgekeerde volgorde — opzoeken in /ToUnicode bfchar en bfrange, codering-stream CID-mapping, Type0-identiteitsmappings, en de vooraf gedefinieerde WinAnsi- en MacRoman-tabellen — om elk vervangend teken terug te zetten in de bytes die het originele lettertype verwacht. De opnieuw gecodeerde bytes worden vervolgens geserialiseerd in een goed gevormde string-literal of hex-string, wat de eigen ontsnappingsregels (escaping rules) van de tokenizer weerspiegelt zodat een parse → re-serialize cyclus stabiel is

De splitsing zelf is chirurgisch in plaats van grootschalig. Alleen het codebyte-bereik dat door de treffer wordt gedekt, wordt vervangen binnen de string-operand; niet-overeenkomende bytes in dezelfde operand, de witruimte tussen tokens en elke omliggende operator blijven letterlijk behouden, byte voor byte. Het vervangen van bca in abcabc levert a + vervanging + bc op, niet een verminkte operand. Vervangingen kunnen korter of langer zijn dan de zoekterm — de literal wordt opnieuw geserialiseerd en de /Length van de stream wordt bijgewerkt — en elke /Contents-stream van een pagina met meerdere streams wordt afzonderlijk verwerkt zodat de pagina goed gevormd blijft

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;

Merk op wat de API niet doet: deze zet de pagina niet opnieuw. PDF heeft geen automatische tekstdoorloop (reflow), dus een vervanging die visueel breder is dan het origineel zal simpelweg meer horizontale ruimte innemen en kan in het gedrang komen met alles wat rechts daarvan is getekend. Vervangingen van gelijke lengte of nagenoeg gelijke lengte — datums, versiestrings, onderdeelnummers, naamscorrecties — zijn de 'sweet spot'. Grootschalige herformulering hoort thuis in het brondocument, niet in de PDF

Waarom kunt u tekst niet vervangen door tekens die de lettertype-subset nooit bevatte?

U kunt tekst niet vervangen door een teken dat de ingesloten lettertype-subset nooit bevatte, omdat de bytevolgorde die dat teken zou selecteren simpelweg niet bestaat in de mappingtabellen van het lettertype. Wanneer een PDF-generator een subset-lettertype insluit, dekken de /ToUnicode CMap en coderingsstructuren alleen de glyphs die het originele document daadwerkelijk gebruikte. HPDFEncodeUnicode kan alleen een mapping omkeren die aanwezig is: als het document nooit de letter E in dat lettertype bevatte, is er geen tekencode for E om naar terug te keren. Dit is een fysieke eigenschap van het bestand, geen beperking van een specifieke bibliotheek — geen enkele tool kan een glyph-mapping tevoorschijn toveren die nooit is ingesloten

HotPDF handelt de mislukking conservatief af. Als een enkel teken van de vervanging niet opnieuw kan worden gecodeerd, wordt die gehele instantie van de zoekterm overgeslagen — geen uitzondering, geen gedeeltelijke wartaal, en de instantie wordt simpelweg niet meegeteld in ReplaceCount. De praktische consequentie: vergelijk ReplaceCount met het aantal treffers van een eerdere zoekopdracht en behandel een tekort als een signaal. In het bovenstaande datumvoorbeeld moet het cijfer 6 ergens in de tekst van het document in datzelfde lettertype voorkomen om de herschrijving te laten slagen — aannemelijk bij een factuur, maar nooit in het algemeen gegarandeerd. Wanneer de tekens die u nodig hebt simpelweg niet beschikbaar zijn en het doel is om gevoelige tekst te verwijderen in plaats van te herformuleren, is echte inhoudsverwijdering sowieso het betere hulpmiddel; zie geladen PDF's redigeren en herstructureren in Delphi voor dat route

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;

De tweede overslagvoorwaarde in dat bericht is de andere gedocumenteerde grens: een zoekterm die meerdere string-operanden overspant — bijvoorbeeld Hello verdeeld over [(He)(llo)] TJ items — wordt wel gevonden door de zoekopdracht, omdat zoeken overeenkomt met de gedecodeerde glyphvolgorde, maar wordt overgeslagen bij vervanging, omdat herschrijven over operandgrenzen heen het samenvoegen van aangrenzende bytebereiken zou vereisen. Eerst zoeken en dan controleren maakt beide limieten zichtbaar in plaats van onzichtbaar

Wat verandert er in het bestand wanneer u opslaat?

Een vervangen /Contents-stream wordt ongecomprimeerd opgeslagen. Met FlateDecode gecomprimeerde streams worden gedecomprimeerd voor bewerking, en wanneer HotPDF de herbouwde bytes schrijft, laat het de vermelding /Filter van de stream vallen en vernieuwt het /Length in plaats van opnieuw te comprimeren. De resulterende PDF is volledig valide en rendert normaal in gangbare viewers; de afweging is een groter bestand voor elke bewerkte stream. Houd bij een batchpijplijn die duizenden documenten verwerkt rekening met die groei of voer stroomafwaarts een afzonderlijke compressieslag uit. Hoe herschreven objecten bij het opslaan reageren op de kruisverwijzingsstructuur van het document is een onderwerp op zich, behandeld in objectstreams en incrementele updates in HotPDF

Al het andere aan het bestand blijft ongemoeid. Onaangetaste streams behouden hun compressie, lettertypen en afbeeldingen worden niet herschreven, and de splitsing op operandniveau betekent dat zelfs de bewerkte streams alleen verschillen van het origineel waar een treffer landde. Dat conservatisme is opzettelijk: hoe meer van een geladen document een bibliotheek herschrijft, hoe meer kansen deze heeft om een generator-eigenaardigheid te verstoren die hij niet had voorzien

Tekst zoeken en vervangen voegt zich bij extractie, redactie en paginarendering in de toolset voor geladen documenten van HotPDF, allemaal aangedreven door dezelfde inhoudsstream-interpreter en beschikbaar van Delphi 5 tot en met de huidige RAD Studio-releases zonder externe afhankelijkheden. De volledige API-referentie en proefversie zijn te vinden op de productpagina van HotPDF Component