Techninis straipsnis

Teksto paieška ir keitimas esamame PDF su Delphi

HotPDF Delphi Component gali ieškoti ir keisti tekstą esamame PDF faile iš Delphi ir C++Builder. SearchLoadedPageText ir SearchLoadedDocumentText glifo tikslumu suranda kiekvieną eilutės pasitaikymą, o ReplaceLoadedPageText ir ReplaceLoadedDocumentText vietoje perrašo rastus baitus, jeigu kiekvienas keitimo simbolis gali būti iš naujo užkoduotas originaliu šriftu — tai fizinis apribojimas, kurį šis straipsnis aprašo sąžiningai, o ne slepia išnašoje

Užduotis, kuri už šios funkcijos slypi, visada yra kasdieniška. Įmonė pasikeičia pavadinimą, o trys tūkstančiai archyve gulinčių sąskaitų vis dar nešioja senąjį. Sutarties šablonas išsiųstas su praėjusių metų galiojimo data. Produkto kodas nebenaudojamas, ir kiekviename jį mininčiame duomenų lape vietoj jo reikia įrašyti kodą įpėdinį. Tekstų rengyklėje kiekvienas iš šių darbų trunka trisdešimt sekundžių. PDF faile tai tikrai sudėtingas uždavinys, o supratimas kodėl skiria gerą API naudojimą nuo klaidos pranešimo, kuris iš tikrųjų yra citata iš specifikacijos

Kodėl teksto keitimas PDF faile toks sudėtingas?

Teksto keitimas PDF faile yra sudėtingas todėl, kad PDF puslapyje nėra redaguojamo teksto — jame yra išdėstyti glifai. Pagal ISO 32000-1 §9.4 teksto rodymo modelį turinio srautas valdo tokius operatorius kaip Tj ir TJ, kurie piešia simbolių kodų sekas teksto matricos nustatytose koordinatėse. Tie kodai nėra Unicode; tai indeksai toje koduotėje, kurią deklaruoja puslapio šriftas, o atgalinis atvaizdis į skaitomus simbolius gali gyventi /ToUnicode CMap struktūroje, koduotės skirtumų masyve arba CID atvaizdžio grandinėje. Nėra jokio pastraipos objekto, jokio teksto tėkmės ir jokios garantijos, kad vienas vizualus žodis apskritai saugomas kaip viena eilutė

Keitimas prie dekodavimo prideda antrą sunkumo sluoksnį: turite tiksliai žinoti, kurie originalaus srauto baitai pagamino kiekvieną glifą, kad galėtumėte įterpti naujus baitus būtent į tą intervalą ir niekur kitur. Teksto ištraukiklis gali sau leisti baitų pozicijas išmesti, kai tik gauna Unicode. Keitiklis negali. Būtent todėl HotPDF šį darbą padalijo per dvi laidas: v2.251.0 pastatė poslinkių sekimo ir paieškos sluoksnį, o v2.252.0 ant jo pastatė perrašymo sluoksnį

Teksto radimas: paieška glifų lygiu su baitų poslinkių sekimu

HotPDF metodas SearchLoadedDocumentText randa kiekvieną ieškomos eilutės pasitaikymą lygindamas su kiekvieno puslapio dekoduota Unicode glifų seka, o ne su neapdorotais srauto baitais, todėl radinys lieka radiniu nepriklausomai nuo to, kaip jį užkodavo šriftas. Po tuo esanti infrastruktūra pristatyta v2.251.0 versijoje: turinio srauto skaidyklė kiekvienam eilutės operandui įsimena StartOfs/EndOfs baitų intervalą, įskaitant jo ( ) arba < > skirtukus, o kiekvienas dekoduotas glifas neša TokenIndex/ItemIndex/ByteOffset trejetą, rodantį atgal į tikslų operandą, TJ masyvo elementą ir jį pagaminusį kodo vienetą. Tas pats glifų interpretatorius varo ištraukimo API, aprašytą straipsnyje teksto ištraukimas iš įkelto PDF Delphi aplinkoje; paieška tiesiog išsaugo tą kilmę, kurią ištraukimas išmeta

Kaip HotPDF paieška Delphi aplinkoje seka StartOfs ir EndOfs baitų intervalus dekoduodama glifus į Unicode THPDFTextMatch rezultatams
HotPDF paieška dirba su dekoduota glifų seka, kartu išlaikydama baitų kilmę, kurios reikia keitimui

Kiekvienas radinys grįžta kaip THPDFTextMatch įrašas, nešantis puslapio indeksą, imtinį glifų intervalą, radinio pradžios X/Y koordinates naudotojo erdvėje ir plotį, šaltinio žymės bei elemento indeksą ir patį rastą tekstą. To pakanka paryškinimo sluoksniui, peržiūros sąsajai ar keitimo žingsniui valdyti. Paieška, kuri nieko neranda, grąžina tuščią masyvą, o ne nulūžta, todėl iškvietimo šablonas lieka paprastas

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;

Vienas sąmoningas projektavimo sprendimas vertas pastabos. Kai CaseSensitive yra False, palyginimas suvienodina raidžių registrą tik ASCII simboliams, ir tai daroma tyčia: pilnas Unicode registro suvienodinimas skirtingai elgiasi Delphi 5 ir XE įrankių grandinėse, kurias HotPDF palaiko, o paieškos API, randanti skirtingus rezultatus priklausomai nuo to, kuris kompiliatorius surinko jūsų aplikaciją, yra blogesnė už tą, kuri turi dokumentuotą ir nuspėjamą ribą. Lotyniško verslo tekstui — pavadinimams, kodams, datoms — ASCII suvienodinimas padengia praktinius atvejus

Teksto keitimas: atvirkštinis kodavimas ir chirurginis įterpimas

ReplaceLoadedDocumentText, pridėtas HotPDF v2.252.0 versijoje, perrašo kiekvieną ieškomos eilutės pasitaikymą leisdamas dekodavimo mechaniką atbuline eiga. Funkcija HPDFEncodeUnicode yra simbolių kodų dekoderio atvirkštinė pusė: ji pereina tą pačią strategijų grandinę priešinga kryptimi — /ToUnicode bfchar ir bfrange paiešką, koduotės srauto CID atvaizdį, Type0 tapatybės atvaizdžius ir iš anksto apibrėžtas WinAnsi bei MacRoman lenteles — kad kiekvieną keitimo simbolį paverstų tais simbolių kodo baitais, kurių tikisi originalus šriftas. Iš naujo užkoduoti baitai tada serializuojami į taisyklingą eilutės literalą arba šešioliktainę eilutę, atkartojant pačios skaidyklės kaitos taisykles, kad analizės ir pakartotinės serializacijos ciklas būtų stabilus

Pats įterpimas yra chirurginis, o ne visa apimantis. Eilutės operande pakeičiamas tik tas kodo baitų intervalas, kurį dengia radinys; nesutampantys to paties operando baitai, tarpai tarp žymių ir kiekvienas aplinkinis operatorius išsaugomi pažodžiui, baitas į baitą. Pakeitus bca eilutėje abcabc gaunama a plius keitinys plius bc, o ne sugadintas operandas. Keitiniai gali būti trumpesni arba ilgesni už ieškomą eilutę — literalas serializuojamas iš naujo, o srauto /Length atnaujinamas — ir kiekvienas daugiasrautinio puslapio /Contents srautas apdorojamas atskirai, kad puslapis liktų taisyklingas

Kaip HotPDF keitimas Delphi aplinkoje apverčia dekodavimo grandinę su HPDFEncodeUnicode ir įterpia tik rastą eilutės operando baitų intervalą
Atvirkštinis kodavimas atkuria šrifto simbolių kodus, o tada perrašomas tik rastas baitų intervalas operando viduje
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;

Atkreipkite dėmesį, ko šis API nedaro: jis iš naujo neišdėsto puslapio. PDF neturi teksto pertekėjimo, todėl keitinys, vizualiai platesnis už originalą, tiesiog užims daugiau horizontalios vietos ir gali suspausti tai, kas nupiešta jam iš dešinės. Tokio paties arba beveik tokio paties ilgio keitiniai — datos, versijų eilutės, dalių numeriai, pavadinimų taisymai — yra pati palankiausia sritis. Didelio masto teksto perrašymas priklauso šaltinio dokumentui, o ne PDF failui

Kodėl negalima pakeisti teksto simboliais, kurių šrifto poaibis niekada neįtraukė?

Negalite pakeisti teksto simboliu, kurio įterptasis šrifto poaibis niekada neįtraukė, nes baitų sekos, kuri tą simbolį parinktų, šrifto atvaizdžio lentelėse tiesiog nėra. Kai PDF gamintojas įterpia šrifto poaibį, jo /ToUnicode CMap ir koduotės struktūros apima tik tuos glifus, kuriuos originalus dokumentas iš tikrųjų naudojo. HPDFEncodeUnicode gali apversti tik tą atvaizdį, kuris yra: jei dokumente tuo šriftu niekada nebuvo raidės E, tai nėra ir simbolio kodo, į kurį E būtų galima apversti. Tai yra fizinė failo savybė, o ne kurios nors bibliotekos apribojimas — joks įrankis negali iš oro sukurti glifo atvaizdžio, kuris niekada nebuvo įterptas

HotPDF tokį nesėkmės atvejį tvarko atsargiai. Jei bent vienas keitinio simbolis negali būti iš naujo užkoduotas, praleidžiamas visas tos ieškomos eilutės pasitaikymas — jokios išimties, jokio dalinio šiukšlių teksto, o pasitaikymas tiesiog neįskaičiuojamas į ReplaceCount. Praktinė pasekmė: palyginkite ReplaceCount su ankstesnės paieškos radinių skaičiumi ir trūkumą traktuokite kaip signalą. Aukščiau pateiktame datos pavyzdyje skaitmuo 6 turi kur nors dokumento tekste pasitaikyti tuo pačiu šriftu, kad perrašymas pavyktų — sąskaitoje tai tikėtina, bendru atveju niekada negarantuota. Kai reikalingų simbolių tiesiog nėra, o tikslas yra pašalinti jautrų tekstą, o ne jį perrašyti, tikras turinio šalinimas vis tiek yra geresnis įrankis; šį kelią aprašo įkeltų PDF redagavimas ir pertvarkymas Delphi aplinkoje

Kodėl HotPDF praleidžia visą Delphi PDF teksto keitimą, kai įterptajame šrifto poaibyje trūksta reikalingo simbolio, ir kaip tai patikrinti per ReplaceCount
Keitinio simboliai, kurių šrifto poaibis negali užkoduoti, praleidžia visą pasitaikymą, todėl ReplaceCount trūkumas yra tikras signalas
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;

Antroji tame pranešime minima praleidimo sąlyga yra kita dokumentuota riba: ieškoma eilutė, išsidriekusi per kelis eilutės operandus, pavyzdžiui Hello, padalytas per [(He)(llo)] TJ elementus, yra randama paieškos, nes paieška lygina dekoduotą glifų seką, tačiau praleidžiama keitimo, nes perrašymas per operandų ribas reikalautų sujungti gretimus baitų intervalus. Paieška ir po jos einantis patikrinimas padaro abi ribas matomas, o ne tylias

Kas faile pasikeičia išsaugant?

Pakeistas /Contents srautas išsaugomas nesuspaustas. FlateDecode suspausti srautai redagavimui išskleidžiami, o kai HotPDF įrašo atstatytus baitus, jis pašalina srauto /Filter įrašą ir atnaujina /Length, užuot suspaudęs iš naujo. Gaunamas PDF yra visiškai galiojantis ir įprastai atvaizduojamas pagrindinėse peržiūros programose; kompromisas yra didesnis failas už kiekvieną redaguotą srautą. Paketiniam konvejeriui, apdorojančiam tūkstančius dokumentų, numatykite tą augimą arba paleiskite atskirą suspaudimo etapą toliau grandinėje. Kaip perrašyti objektai išsaugant sąveikauja su dokumento kryžminių nuorodų struktūra, yra atskira tema, aprašyta straipsnyje objektų srautai ir laipsniški atnaujinimai HotPDF

Visa kita faile paliekama ramybėje. Nepaliesti srautai išlaiko savo suspaudimą, šriftai ir paveikslėliai neperrašomi, o operando lygio įterpimas reiškia, kad net redaguoti srautai nuo originalo skiriasi tik ten, kur pataikė radinys. Šis konservatyvumas yra sąmoningas: kuo daugiau įkelto dokumento biblioteka perrašo, tuo daugiau progų ji turi sugadinti kokį nors gamintojo keistumą, kurio nenumatė

Teksto paieška ir keitimas prisijungia prie ištraukimo, redagavimo ir puslapių atvaizdavimo HotPDF įkeltų dokumentų įrankiuose; visus juos varo tas pats turinio srauto interpretatorius, ir jie pasiekiami nuo Delphi 5 iki dabartinių RAD Studio laidų be išorinių priklausomybių. Pilnas API aprašas ir bandomoji versija yra HotPDF Delphi komponento produkto puslapyje