Műszaki cikk

Szöveg keresése és cseréje meglévő PDF-ben Delphi-vel

A HotPDF Component képes keresni és cserélni a szöveget egy meglévő PDF-en birodalmon belül Delphi és C++Builder környezetből; a SearchLoadedPageText és SearchLoadedDocumentText karakterszintű pontossággal keresi meg a karakterlánc összes előfordulását, a ReplaceLoadedPageText és ReplaceLoadedDocumentText pedig a helyén írja újra az egyező bájtokat — feltéve, hogy minden helyettesítő karakter újra kódolható az eredeti betűtípuson keresztül, ez egy olyan fizikai korlát, amelyet ez a cikk őszintén tárgyal, nem pedig egy lábjegyzetben rejt el

A funkció mögötti igény mindig hétköznapi; egy vállalat átnevezi magát, és háromezer archivált számla még mindig a régi nevet viseli; a szerződéssablon lejárt lejárati dátummal lett kiküldve; a termékkódot kivonták a forgalomból, és minden olyan adatlapnak, amely megemlíti azt, inkább az utód kódra van szüksége; egy szövegszerkesztőben ezek mindegyike egy harminc másodperces munka; a PDF-ben ez valóban nehéz probléma, és annak megértése, hogy miért az, jelenti a különbséget az API helyes használata és egy olyan hibajelentés benyújtása között, amely valójában a specifikáció idézése

Why is replacing text in a PDF so hard?

Miért olyan nehéz a szöveg cseréje a PDF-ben?

A szöveg cseréje a PDF-ben azért nehéz, mert a PDF-oldal nem tartalmaz szerkeszthető szöveget — pozicionált karaktereket tartalmaz; az ISO 32000-1 §9.4 szövegmegjelenítési modellje szerint a tartalomfolyam olyan operátorokat hajt meg, mint a Tj és a TJ, amelyek karakterkódok sorozatait rajzolják ki a szövegmátrix által meghatározott koordinátákra; ezek a kódok nem Unicode-ok; ezek indexek abba a kódolásba, amelyet az oldal betűtípusa deklarál, a olvasható karakterekké történő visszaleképezés pedig a /ToUnicode CMap-ben, egy kódolási eltérési tömbben vagy egy CID-leképezési láncban lakhat; nincs bekezdés-objektum, nincs szövegáramlás, és nincs garancia arra, hogy egy vizuális szó egyetlen karakterláncként tárolódik

A csere egy második nehézségi réteget ad a dekódolásra: pontosan tudnia kell, hogy az eredeti adatfolyam-bájtok mely része hozta létre az egyes karaktereket, így pontosan abba a tartományba tudja beilleszteni az új bájtokat, és semmi másba; a szövegkinyerő megengedheti magának, hogy eldobja a bájtpozíciókat, miután kinyerte a Unicode-ot; a cserélő nem teheti meg; ezért a HotPDF két kiadásra osztotta a munkát — a v2.251.0 építette fel az eltolás-követést és a keresési réteget, a v2.252.0 pedig a ráépülő újraírási réteget

Szöveg keresése: karakterszintű keresés bájteltolás-követéssel

A HotPDF SearchLoadedDocumentText metódusa az egyes oldalak dekódolt Unicode karaktersorozatával való egyeztetéssel keresi meg a keresett kifejezés minden előfordulását, nem pedig a nyers adatfolyam-bájtok alapján, így a találat független attól, hogy a betűtípus hogyan kódolta azt; az alapjául szolgáló infrastruktúra a v2.251.0 verzióban mutatkozott be: a tartalomfolyam-értelmező rögzíti az egyes karakterlánc-operandusok StartOfs/EndOfs bájttartományát — beleértve a ( ) vagy < > határolójeleket is —, és minden dekódolt karakter egy TokenIndex/ItemIndex/ByteOffset hármast hordoz, amely a pontos operandusra, a TJ-tömb elemére és az azt előállító kódegységre mutat vissza; ugyanaz a karakterértelmező hajtja meg a szöveg betöltött PDF-ből történő kinyeréséről szóló cikkben leírt kinyerési API-t is; a keresés egyszerűen megtartja azt a származási helyet, amelyet a kinyerés eldob

Minden egyezés egy THPDFTextMatch rekordként érkezik vissza, amely hordozza az oldalindexet, az inkluzív karaktertartományt, a találat felhasználói térbeli X/Y kezdőpontját és szélességét, a forrás token- és elemszámát, valamint magát a talált szöveget; ez elegendő egy kiemelő réteg, egy ellenőrző felület vagy a cserélő lépés meghajtásához; az a keresés, amely nem talál semmit, üres tömböt ad vissza hiba helyett, így a hívási minta egyszerű marad

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;

Egy szándékos tervezési döntést érdemes megjegyezni; amikor a CaseSensitive értéke False, az összehasonlítás szándékosan csak az ASCII karaktereknél hajtja végre a kis- és nagybetűk egységesítését: a teljes Unicode kis- és nagybetűs egységesítés eltérően működik a HotPDF által támogatott Delphi 5-től az XE-ig terjedő eszközláncok között, és egy olyan keresési API, amely attól függően talál eltérő egyezéseket, hogy melyik fordító készítette az alkalmazását, rosszabb, mint a dokumentált, kiszámítható korláttal rendelkező; a latin nyelvű üzleti szövegek — nevek, kódok, dátumok — esetében az ASCII egységesítés lefedi a gyakorlati eseteket

Szöveg cseréje: fordított kódolás és sebészeti összefűzés

A HotPDF v2.252.0 verziójában megjelent ReplaceLoadedDocumentText a dekódoló mechanizmus hátrafelé történő futtatásával írja újra a keresett szó összes előfordulását; a HPDFEncodeUnicode függvény a karakterkód-dekóder ellentéte: ugyanazon stratégiai láncon megy végig visszafelé — /ToUnicode bfchar és bfrange keresés, kódoló adatfolyam CID leképezés, Type0 azonossági leképezések, valamint az előre meghatározott WinAnsi and MacRoman táblák —, hogy minden helyettesítő karaktert visszaalakítson a karakterkód-bájtokká, amelyeket az eredeti betűtípus elvár; az újrakódolt bájtok ezután egy szabályos karakterlánc-literállá vagy hexadecimális karakterlánccá alakulnak, tükrözve az értelmező saját menekülési (escaping) szabályait, így az elemzés → újra-sorosítás kétirányú útja stabil marad

Minden egyes csere sebészeti jellegű, nem pedig nagyüzemi; a karakterlánc-operanduson belül csak az egyezés által lefedett kód-bájt tartomány kerül cserére; ugyanazon operandus nem egyező bájtjai, a tokenek közötti szóközök és minden környező operátor pontosan, bájtról bájtra megmarad; a bca cseréje az abcabc-ben a következőt eredményezi: a + csere + bc, nem pedig egy tönkretett operandust; a cserék lehetnek rövidebbek vagy hosszabbak is, mint a keresett szó — a literál újra-sorosításra kerül és az adatfolyam /Length-e frissül —, és a több adatfolyamból álló oldal minden egyes /Contents adatfolyamát külön dolgozza fel, így az oldal szabályos marad

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;

Vegye figyelembe, amit az API nem tesz meg: nem szedi újra az oldalt; a PDF-ben nincs folyószöveg-kezelés, így a vizuálisan az eredetinél szélesebb csere egyszerűen több vízszintes helyet fog elfoglalni, és összezsúfolódhat a tőle jobbra lévő tartalommal; az azonos hosszúságú vagy közeli hosszúságú cserék — dátumok, verzió-karakterláncok, alkatrészszámok, névjavítások — jelentik az optimális területet; a teljes átfogalmazás a forrásdokumentumhoz tartozk, nem pedig a PDF-hez

Miért nem cserélhető a szöveg olyan karakterre, amelyet a betűtípus-részhalmaz soha nem tartalmazott?

Nem cserélheti le a szöveget olyan karakterre, amelyet a beágyazott betűtípus-részhalmaz soha nem tartalmazott, mivel az a bájtsorozat, amely kiválasztaná az adott karaktert, egyszerűen nem létezik a betűtípus leképezési tábláiban; amikor egy PDF előállító részhalmaz-betűtípust ágyaz be, annak /ToUnicode CMap-je és kódolási struktúrái csak azokat a karaktereket fedik le, amelyeket az eredeti dokumentum valóban használt; a HPDFEncodeUnicode csak a meglévő leképezést tudja megfordítani: ha a dokumentum soha nem tartalmazta az E betűt abban a betűtípusban, nincs olyan karakterkód az E-hez, amelyre vissza lehetne alakítani; ez a fájl fizikai tulajdonsága, nem pedig egy adott könyvtár korlátja — semmilyen eszköz sem tud elővarázsolni olyan karakterleképezést, amely soha nem volt beágyazva

A HotPDF konzervatívan kezeli a hibát; ha a csere egyetlen karaktere sem kódolható újra, a keresett kifejezés teljes előfordulása kihagyásra kerül — nincs kivétel, nincs részleges zagyva szöveg, és az előfordulást egyszerűen nem számolja bele a ReplaceCount értékébe; a gyakorlati következmény: ellenőrizze a ReplaceCount értékét a korábbi keresés találati számával szemben, és kezelje a hiányt jelzésként; a fenti dátumpéldában a 6-os számnak valahol szerepelnie kell a dokumentum szövegében ugyanabban a betűtípusban ahhoz, hogy az újraírás sikeres legyen — ez számla esetén valószínű, de általában nem garantált; ha a szükséges karakterek egyszerűen nem állnak rendelkezésre, és a cél a bizalmas szöveg eltávolítása, nem pedig az átfogalmazás, a tartalom valódi eltávolítása amúgy is jobb eszköz; erről a betöltött PDF-ek kitakarásáról és szerkezetének átalakításáról Delphi-ben szóló cikkben olvashat

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;

Az üzenetben szereplő második kihagyási feltétel a másik dokumentált határ: a több karakterlánc-operanduson átnyúló kifejezés — például a Hello felosztva a [(He)(llo)] TJ elemek között — a keresés során megtalálható, mert a keresés a dekódolt karaktersorozattal egyezik, de a csere kihagyja azt, mivel az operandushatárokon átnyúló újraírás megkövetelné a szomszédos bájttartományok összevonását; a keresés-majd-ellenőrzés mindkét korlátot láthatóvá teszi ahelyett, hogy csendben maradna

Mi változik a fájlban a mentéskor?

A lecserélt /Contents adatfolyam tömörítetlenül kerül mentésre; a FlateDecode-tömörített adatfolyamok kitömörítésre kerülnek a szerkesztéshez, és amikor a HotPDF leírja az újjáépített bájtokat, elhagyja az adatfolyam /Filter bejegyzését és frissíti a /Length-et az újratömörítés helyett; a kapott PDF teljesen érvényes, és normálisan renderelődik a fősodorbeli megjelenítőkben; a kompromisszum a nagyobb fájlméret minden egyes szerkesztett adatfolyam esetében; az olyan kötegelt folyamatoknál, amelyek megkövetelik a tömörséget, számoljon ezzel a növekedéssel, vagy futtasson le egy külön tömörítési lépést a feldolgozás végén; az, hogy az újraírt objektumok hogyan lépnek interakcióba a dokumentum kereszthivatkozási struktúrájával a mentés során, külön téma, amelyet a HotPDF objektumfolyamairól és inkrementális frissítéseiről szóló cikk tárgyal

A fájlban minden más érintetlen marad; a nem módosított adatfolyamok megőrzik tömörítésüket, a betűtípusok és a képek nem íródnak újra, és az operandus-szintű összefűzés azt jelenti, hogy még a szerkesztett adatfolyamok is csak ott térnek el az eredetitől, ahol a találat történt; ez a konzervativizmus szándékos: minél több részt ír újra egy könyvtár egy betöltött dokumentumból, annál több lehetősége van arra, hogy elrontson egy olyan előállítói sajátosságot, amelyet nem látott előre

A szöveges keresés és csere a kinyeréssel, a kitakarással és az oldal-rendereléssel együtt része a HotPDF betöltött dokumentumokat kezelő eszközkészletének, amelyet ugyanaz a tartalomfolyam-értelmező hajt meg, és elérhető a Delphi 5-től a jelenlegi RAD Studio kiadásokig, külső függőségek nélkül; a teljes API referencia és a próbaverzió letöltése a HotPDF Component termékoldalán található