Műszaki cikk

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

A HotPDF Delphi Component Delphiből és C++Builderből is képes szöveget keresni és cserélni egy meglévő PDF-en belül. A SearchLoadedPageText és a SearchLoadedDocumentText glifaszintű pontossággal találja meg egy karakterlánc minden előfordulását, a ReplaceLoadedPageText és a ReplaceLoadedDocumentText pedig helyben írja át az illeszkedő bájtokat — feltéve, hogy minden csere-karakter újrakódolható az eredeti betűtípuson keresztül; ezt a fizikai korlátot ez a cikk őszintén tárgyalja, nem pedig lábjegyzetbe rejti

A funkció mögötti kérés mindig hétköznapi. Egy cég nevet vált, és háromezer archivált számla még mindig a régi nevet viseli. Egy szerződéssablon a tavalyi lejárati dátummal került ki. Egy termékkódot kivontak a forgalomból, és minden adatlapon, amely említi, az utódkódnak kell szerepelnie. Egy szövegszerkesztőben mindegyik harminc másodperces munka. Egy PDF-ben viszont valóban nehéz feladat, és annak megértése, hogy miért, az a különbség, ami az API jó használata és egy olyan hibajelentés beküldése között van, amely valójában szabványhivatkozás

Miért ilyen nehéz szöveget cserélni egy PDF-ben?

A szövegcsere azért nehéz egy PDF-ben, mert a PDF-oldal nem szerkeszthető szöveget tartalmaz, hanem pozicionált glifákat. Az ISO 32000-1 §9.4 szövegmegjelenítési modellje szerint a tartalomfolyam olyan operátorokat vezérel, mint a Tj és a TJ, amelyek karakterkód-sorozatokat festenek ki a szövegmátrix által meghatározott koordinátákon. Ezek a kódok nem Unicode-értékek; indexek abba a kódolásba, amelyet az oldal betűtípusa deklarál, és az olvasható karakterekre visszavezető leképezés élhet egy /ToUnicode CMap-ben, egy kódolási különbségtömbben vagy egy CID-leképezési láncban. Nincs bekezdésobjektum, nincs szövegfolyam, és semmi nem garantálja, hogy egy vizuálisan összetartozó szó egyetlen karakterláncként van tárolva

A csere a dekódolás fölé egy második nehézségi réteget húz: pontosan tudnia kell, hogy az eredeti adatfolyam mely bájtjai állították elő az egyes glifákat, hogy az új bájtokat pontosan abba a tartományba és sehová máshová illeszthesse be. Egy szövegkinyerő megengedheti magának, hogy eldobja a bájtpozíciókat, amint megvan a Unicode. Egy cserélő nem. Ezért osztotta szét a HotPDF a munkát két kiadásra — a v2.251.0 építette fel az eltoláskövető és kereső réteget, a v2.252.0 pedig az erre épülő átíró réteget

Szöveg megtalálása: glifaszintű keresés bájteltolás-követéssel

A HotPDF SearchLoadedDocumentText metódusa úgy találja meg a keresett minta minden előfordulását, hogy az egyes oldalak dekódolt Unicode glifasorozatához illeszt, nem a nyers adatfolyam-bájtokhoz, így a találat találat marad függetlenül attól, hogyan kódolta a betűtípus. Az alatta lévő infrastruktúra a v2.251.0 verzióban jelent meg: a tartalomfolyam tokenizálója minden karakterlánc-operandushoz feljegyez egy StartOfs/EndOfs bájttartományt — a ( ) vagy < > határolóival együtt —, és minden dekódolt glifa hordoz egy TokenIndex/ItemIndex/ByteOffset hármast, amely pontosan arra az operandusra, TJ-tömbelemre és kódegységre mutat vissza, amely előállította. Ugyanez a glifaértelmező hajtja meg a betöltött PDF-ből történő szövegkinyerést Delphiben ismertető cikkben leírt kinyerési API-t is; a keresés egyszerűen megőrzi azt a származási információt, amelyet a kinyerés eldob

Hogyan követi a HotPDF keresése Delphiben a StartOfs és EndOfs bájttartományokat, miközben a glifákat Unicode-ra dekódolja a THPDFTextMatch eredményekhez
A HotPDF keresése a dekódolt glifasorozaton dolgozik, miközben megőrzi azt a bájtszintű származást, amelyre a cserének szüksége van

Minden találat egy THPDFTextMatch rekordként érkezik vissza, amely hordozza az oldalindexet, a zárt glifatartományt, a találat felhasználói tér szerinti X/Y kezdőpontját és szélességét, a forrástoken és -elem indexét, valamint magát az illeszkedő szöveget. Ez elég ahhoz, hogy kiemelőréteget, ellenőrző felületet vagy a cserelépést hajtsa meg. Az a keresés, amely semmit nem talál, üres tömböt ad vissza hibajelzés 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és külön megjegyzést érdemel. Amikor a CaseSensitive értéke False, az összehasonlítás szándékosan csak az ASCII karakterek kis- és nagybetűit vonja össze: a teljes Unicode kis-nagybetű-összevonás eltérően viselkedik a HotPDF által támogatott Delphi 5 – XE eszközláncokon, és az a kereső API, amely attól függően talál más-más egyezéseket, hogy melyik fordító készítette az alkalmazását, rosszabb annál, amelynek dokumentált, kiszámítható korlátja van. A latin betűs üzleti szövegeknél — nevek, kódok, dátumok — az ASCII-összevonás lefedi a gyakorlati eseteket

Szöveg cseréje: fordított kódolás és sebészi beillesztés

A HotPDF v2.252.0 verziójában megjelent ReplaceLoadedDocumentText úgy írja át a keresett minta minden előfordulását, hogy visszafelé futtatja a dekódoló gépezetet. A HPDFEncodeUnicode függvény a karakterkód-dekódoló inverze: ugyanazt a stratégialáncot járja be fordított irányban — /ToUnicode bfchar és bfrange keresés, kódolási adatfolyam CID-leképezése, Type0 identitásleképezések, valamint az előre definiált WinAnsi és MacRoman táblák —, hogy minden csere-karaktert visszaalakítson azokká a karakterkód-bájtokká, amelyeket az eredeti betűtípus vár. Az újrakódolt bájtok ezután jól formált karakterlánc-literállá vagy hexadecimális karakterlánccá sorosodnak, tükrözve magának a tokenizálónak az escape-szabályait, hogy az elemzés → újrasorosítás körút stabil legyen

Maga a beillesztés sebészi, nem pedig nagybani. A karakterlánc-operanduson belül csak a találat által lefedett kódbájt-tartomány cserélődik; az ugyanabban az operandusban lévő nem illeszkedő bájtok, a tokenek közötti szóközök és minden környező operátor bájtról bájtra változatlan marad. A bca cseréje az abcabc szövegben a + csere + bc eredményt ad, nem szétvert operandust. A csere lehet rövidebb vagy hosszabb is a keresett mintánál — a literál újrasorosodik, és az adatfolyam /Length értéke frissül —, és egy többfolyamos oldal minden /Contents adatfolyama elkülönítve dolgozódik fel, hogy az oldal jól formált maradjon

Hogyan fordítja meg a HotPDF cseréje Delphiben a dekódolási láncot a HPDFEncodeUnicode függvénnyel, és hogyan illeszti be a karakterlánc-operandusnak csak az illeszkedő bájttartományát
A fordított kódolás újraépíti a betűtípus karakterkódjait, majd az operanduson belül csak az illeszkedő bájttartomány íródik át
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 észre, mit nem tesz meg az API: nem szedi újra az oldalt. A PDF-ben nincs újratördelés, így az a csere, amely vizuálisan szélesebb az eredetinél, egyszerűen több vízszintes helyet foglal el, és összezsúfolhatja azt, amit tőle jobbra festettek ki. Az azonos vagy közel azonos hosszúságú helyettesítések — dátumok, verziószámok, cikkszámok, névjavítások — jelentik az édes pontot. A nagybani újrafogalmazás a forrásdokumentumba való, nem a PDF-be

Miért nem cserélhető a szöveg olyan karakterekre, amelyeket a betűtípus-részhalmaz sosem tartalmazott?

Azért nem cserélheti le a szöveget olyan karakterre, amelyet a beágyazott betűtípus-részhalmaz sosem tartalmazott, mert az a bájtsorozat, amely azt a karaktert kiválasztaná, 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 szerkezetei csak azokat a glifákat fedik le, amelyeket az eredeti dokumentum ténylegesen használt. A HPDFEncodeUnicode csak olyan leképezést tud megfordítani, amely jelen van: ha a dokumentumban abban a betűtípusban soha nem szerepelt az E betű, akkor nincs karakterkód, amelyre az E visszavezethető lenne. Ez a fájl fizikai tulajdonsága, nem egy adott könyvtár korlátja — egyetlen eszköz sem tud olyan glifaleképezést előhúzni, amelyet soha nem ágyaztak be

A HotPDF óvatosan kezeli a sikertelenséget. Ha a csere akár egyetlen karaktere sem kódolható újra, a keresett minta teljes előfordulása kimarad — nincs kivétel, nincs részleges zagyvaság, és az előfordulás egyszerűen nem számít bele a ReplaceCount értékébe. A gyakorlati következmény: vesse össze a ReplaceCount értéket egy megelőző keresés találatszámával, és tekintse a hiányt jelzésnek. A fenti dátumpéldában a 6 számjegynek szerepelnie kell valahol a dokumentum szövegében ugyanazzal a betűtípussal, hogy az átírás sikerüljön — egy számlán ez valószínű, általánosságban soha nem garantált. Amikor a szükséges karakterek egyszerűen nem állnak rendelkezésre, és a cél inkább érzékeny szöveg eltávolítása, mint átfogalmazása, a valódi tartalomeltávolítás amúgy is jobb eszköz; ehhez az útvonalhoz lásd a betöltött PDF-ek kitakarásáról és átszerkesztéséről Delphiben szóló cikket

Miért hagyja ki a HotPDF a teljes Delphi PDF szövegcserét, amikor a beágyazott betűtípus-részhalmazból hiányzik egy szükséges karakter, és hogyan ellenőrizhető ez a ReplaceCount értékkel
Azok a csere-karakterek, amelyeket a betűtípus-részhalmaz nem tud újrakódolni, a teljes előfordulást kihagyják, így a ReplaceCount hiánya valódi jelzés
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: az a keresett minta, amely több karakterlánc-operanduson ível át — például a [(He)(llo)] TJ elemek között szétvágott Hello —, a keresés számára megtalálható, mert a keresés a dekódolt glifasorozathoz illeszt, a csere viszont kihagyja, mert az operandushatárokon átnyúló átírás szomszédos bájttartományok összevonását igényelné. A keresés–majd–ellenőrzés menete mindkét korlátot láthatóvá teszi ahelyett, hogy csendben maradnának

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

A lecserélt /Contents adatfolyam tömörítetlenül mentődik. A FlateDecode-dal tömörített adatfolyamok a szerkesztéshez kicsomagolódnak, és amikor a HotPDF kiírja az újraépített bájtokat, elhagyja az adatfolyam /Filter bejegyzését, és frissíti a /Length értéket ahelyett, hogy újratömörítene. Az így kapott PDF teljesen érvényes, és a mainstream megjelenítőkben normálisan renderelődik; a kompromisszum minden szerkesztett adatfolyamnál nagyobb fájlméret. Egy több ezer dokumentumot feldolgozó kötegelt folyamatnál tervezzen ezzel a növekedéssel, vagy futtasson külön tömörítési menetet a lánc végén. Hogy az átírt objektumok mentéskor hogyan hatnak a dokumentum kereszthivatkozási szerkezetére, az önálló téma, amelyet az objektumfolyamokról és inkrementális frissítésekről a HotPDF-ben szóló cikk tárgyal

A fájl minden más része érintetlen marad. Az érintetlen adatfolyamok megtartják a tömörítésüket, a betűtípusok és a képek nem íródnak újra, az operandusszintű beillesztés pedig azt jelenti, hogy még a szerkesztett adatfolyamok is csak ott térnek el az eredetitől, ahol találat volt. Ez az óvatosság szándékos: minél többet ír át egy könyvtár egy betöltött dokumentumból, annál több alkalma van elrontani egy olyan előállítói furcsaságot, amelyre nem számított

A szövegkeresés és -csere a kinyeréshez, a kitakaráshoz és az oldalrendereléshez csatlakozik a HotPDF betöltött dokumentumokra szánt eszközkészletében; mindegyiket ugyanaz a tartalomfolyam-értelmező hajtja, és Delphi 5-től a jelenlegi RAD Studio kiadásokig külső függőségek nélkül elérhető. A teljes API-referencia és a próbaverzió letöltése a HotPDF Delphi Component termékoldalán található