Teknisk artikel

Søg og erstat tekst i en eksisterende PDF med Delphi

HotPDF Component kan søge og erstatte tekst i en eksisterende PDF fra Delphi og C++Builder. SearchLoadedPageText og SearchLoadedDocumentText finder enhver forekomst af en streng med præcision på tegnniveau (glyph-level), og ReplaceLoadedPageText og ReplaceLoadedDocumentText omskriver de matchede bytes på stedet — forudsat at hvert erstatningstegn kan genkodes via den oprindelige skrifttype, en fysisk begrænsning, som denne artikel behandler ærligt frem for at gemme væk i en fodnote

Kravet bag denne funktion er altid helt jordnært. En virksomhed skifter navn, og tre tusinde arkiverede fakturaer bærer stadig det gamle navn. En kontraktskabelon blev sendt ud med sidste års udløbsdato. En produktkode udgik, og ethvert datablad, der nævner den, har brug for den nye kode i stedet. I et tekstbehandlingsprogram er hver af disse opgaver klaret på tredive sekunder. I en PDF er det et reelt svært problem, og forståelsen af hvorfor gør forskellen på at bruge API'en korrekt eller at indsende en fejlrapport, der reelt blot er et citat fra specifikationen

Hvorfor er det så svært at erstatte tekst i en PDF?

Det er svært at erstatte tekst i en PDF, fordi en PDF side ikke indeholder redigerbar tekst — den indeholder placerede tegn (glyphs). Under tekstvisningsmodellen i ISO 32000-1 §9.4 driver en indholdsstrøm operatorer som Tj og TJ, der tegner sekvenser af tegnkoder ved koordinater fastlagt af tekstmatrixen. Disse koder er ikke Unicode; de er indeks i den kodning, som sidens skrifttype erklærer, og mappingen tilbage til læsbare tegn kan ligge i en /ToUnicode CMap, et array med kodningsdifferencer eller en CID-mappingskæde. Der findes intet afsnitsobjekt, intet tekstflow og ingen garanti for, at et visuelt ord overhovedet gemmes som én enkelt streng

Erstatning tilføjer et ekstra lag af vanskelighed oven på afkodningen: Du skal vide præcis, hvilke bytes i den oprindelige strøm der producerede hvert enkelt tegn, så du kan splejse nye bytes ind i netop det område og intet andet. En tekstudtrækker har råd til at kassere byte-positionerne, når først Unicode-teksten er hentet ud. Det har en erstatningsfunktion ikke. Det er grunden til, at HotPDF opdelte arbejdet over to udgivelser — v2.251.0 opbyggede offset-sporingen og søgelaget, og v2.252.0 byggede omskrivningslaget ovenpå

Søgning efter tekst: Søgning på tegnniveau med sporing af byte-offset

HotPDFs SearchLoadedDocumentText finder enhver forekomst af et søgeord ved at matche mod den afkodede Unicode-tegnsekvens på hver side, ikke mod de rå strømbytes, så et hit is et hit, uanset hvordan skrifttypen har kodet det. Infrastrukturen under blev introduceret i v2.251.0: Indholdsstrøm-tokenizeren registrerer et StartOfs/EndOfs-byteområde for hver strengoperand — inklusive dens separatorer ( ) eller < > — og hvert afkodet tegn bærer en TokenIndex/ItemIndex/ByteOffset-tredobbelt værdi, der peger tilbage på den præcise operand, TJ-array-post og kodeenhed, der producerede det. Den samme tegnfortolker driver udtræknings-API'en beskrevet i udtrækning af tekst fra en indlæst PDF i Delphi; søgningen bevarer blot de oprindelsesdata, som udtrækningen kasserer

Hvert match kommer back som en THPDFTextMatch-post, der indeholder sideindeks, det inklusive tegnområde, hit'ets X/Y-udgangspunkt og bredde i brugerrummet, kildetoken- og postindeks samt selve den matchede tekst. Det er nok til at drive en fremhævningsmarkering, en brugergrænseflade til gennemsyn eller erstatningstrinnet. En søgning, der ikke finder noget, returnerer et tomt array frem for at fejle, så kalder-mønsteret forbliver enkelt

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;

Et bevidst designvalg fortjener en bemærkning. Når CaseSensitive er False, udfører sammenligningen kun versalfoldsning (case folding) for ASCII-tegn: Fuld Unicode-versalfoldsning opfører sig forskelligt på tværs af Delphi 5- til XE-værktøjskæderne, som HotPDF understøtter, og et søge-API, der finder forskellige matches afhængigt af, hvilken compiler der byggede din applikation, er værre end et med en dokumenteret, forudsigelig grænse. For latinsk forretningstekst — navne, koder, datoer — ASCII-foldsning dækker de praktiske tilfælde

Erstatning af tekst: Omvendt kodning og kirurgisk splejsning

ReplaceLoadedDocumentText, tilføjet i HotPDF v2.252.0, omskriver enhver forekomst af et søgeord ved at køre afkodningsmaskineriet baglæns. Funktionen HPDFEncodeUnicode er det omvendte af tegnkodeafkoderen: Den gennemgår den samme strategikæde i omvendt rækkefølge — opslag i /ToUnicode bfchar og bfrange, kodningsstrøm-CID-mapping, Type0-identitetsmappings samt de foruddefinerede WinAnsi- og MacRoman-tabeller — for at omdanne hvert erstatningstegn tilbage til de tegnkode-bytes, som den oprindelige skrifttype forventer. De genkodede bytes serialiseres derefter til en korrekt formateret strengliteral eller hex-streng, hvilket afspejler tokenizerens egne escaping-regler, så en fortolk -> re-serialiser rundtur er stabil

Selve splejsningen er kirurgisk frem for en total udskiftning. Kun det kode-byteområde, der dækkes af matchet, erstattes inde i strengoperanden; ikke-matchede bytes i samme operand, det hvide rum mellem tokens og enhver omgivende operator bevares ordret, byte for byte. Erstatning af bca i abcabc giver a + erstatning + bc, ikke en ødelagt operand. Erstatninger kan være kortere eller længere end søgeordet — strengliteralen re-serialiseres, og strømmens /Length opdateres — og hver enkelt /Contents-strøm på en side med flere strømme behandles isoleret, så siden forbliver korrekt formateret

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;

Bemærk, hvad API'en ikke gør: Den ombryder ikke siden igen. PDF har intet tekstflow (reflow), så en erstatning, der er visuelt bredere end den oprindelige, vil blot optage mere vandret plads og kan presse det, der var tegnet til højre for den. Substitutioner af samme eller næsten samme længde — datoer, versionsstrenge, varenumre, navnerettelser — er det ideelle anvendelsesområde. Omfattende tekstændringer hører hjemme i kildedokumentet, ikke i PDF-filen

Hvorfor kan man ikke erstatte tekst med tegn, som skrifttypens delmængde aldrig indeholdt?

Du kan ikke erstatte tekst med et tegn, som den integrerede skrifttypedelmængde aldrig indeholdt, fordi den bytesekvens, der ville vælge det pågældende tegn, simpelthen ikke eksisterer i skrifttypens mapping-tabeller. Når en PDF-producent integrerer en delmængdeskrifttype, dækker dens /ToUnicode CMap- og kodningsstrukturer kun de tegn (glyphs), som det oprindelige dokument rent faktisk brugte. HPDFEncodeUnicode kan kun vende en mapping, der er til stede: Hvis dokumentet aldrig indeholdt bogstavet E i den pågældende skrifttype, findes der ingen tegnkode for E at vende tilbage til. Dette er en fysisk egenskab ved filen, ikke en begrænsning i et bestemt bibliotek — intet værktøj kan fremtrylle en tegnmapping, der aldrig var integreret

HotPDF håndterer denne fejl konservativt. Hvis et enkelt tegn i erstatningen ikke kan genkodes, springes hele forekomsten over — ingen undtagelse, intet delvist ødelagt tekst, og forekomsten tælles simpelthen ikke med i ReplaceCount. Den praktiske konsekvens: Sammenlign ReplaceCount med antallet af hits fra en forudgående søgning, og behandl en eventuel difference som et signal. I datoeksemplet ovenfor skal tallet 6 optræde et eller andet sted i dokumentets tekst i den samme skrifttype, for at omskrivningen lykkes — sandsynligvis i en faktura, men aldrig garanteret generelt. Når de tegn, du skal bruge, simpelthen ikke er tilgængelige, og målet er at fjerne følsom tekst frem for at omformulere den, er ægte indholdsrensning alligevel det bedste værktøj; se redigering og omstrukturering af indlæste PDF-filer i Delphi for den vej

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;

Den anden spring-over-betingelse i denne meddelelse er den anden dokumenterede grænse: Et søgeord, der strækker sig over flere strengoperander — f.eks. Hello opdelt over elementerne i [(He)(llo)] TJ — findes af søgefunktionen, fordi søgningen matcher den afkodede tegnsekvens, men springes over af erstatningsfunktionen, fordi omskrivning på tværs af operandgrænser ville kræve sammenlægning af tilstødende byteområder. Søg-og-verificer gør begge grænser synlige frem for at ske i det skjulte

Hvad ændrer sig i filen, når du gemmer?

En erstattet /Contents-strøm gemmes ukomprimeret. FlateDecode-komprimerede strømme dekomprimeres til redigering, og når HotPDF skriver de genopbyggede bytes, fjerner den strømmens /Filter-post og opdaterer /Length i stedet for at genkomprimere. Den resulterende PDF er fuldt gyldig og renderer normalt i gængse fremvisere; kompromiset er en større fil for hver redigeret strøm. For en batchpipeline, der behandler tusindvis af dokumenter, skal man indregne denne vækst eller køre en separat komprimeringskørsel efterfølgende. Hvordan omskrevne objektter interagerer med dokumentets krydsreferencestruktur ved lagring, er sit eget emne, som dækkes i objektstrømme og inkrementelle opdateringer i HotPDF

Alt andet i filen efterlades urørt. Strømme, der ikke er rørt, beholder deres komprimering, skrifttyper og billeder omskrives ikke, og splejsningen på operandniveau betyder, at selv de redigerede strømme kun adskiller sig fra originalen der, hvor et match landede. Denne konservatisme er bevidst: Jo mere et indlæst dokument omskrives af et bibliotek, jo flere muligheder er der for at ødelægge en særhed hos producenten, som man ikke havde forudset

Tekstsøgning og -erstatning supplerer udtrækning, redigering og siderendering i HotPDFs værktøjssæt til indlæste dokumenter, alt sammen drevet af den samme indholdsstrøm-fortolker og tilgængeligt fra Delphi 5 til de nuværende RAD Studio-udgivelser uden eksterne afhængigheder. Den fulde API-reference og prøveversion kan hentes på produktsiden for HotPDF Component