Tehnični članak

Iskanje in zamenjava besedila v obstoječem PDF-ju z Delphijem

HotPDF Component lahko poišče in zamenja besedilo v obstoječem PDF-ju iz Delphija in C++Builderja. Klica SearchLoadedPageText in SearchLoadedDocumentText poiščeta vsako pojavitev niza z natančnostjo na ravni glifa, klica ReplaceLoadedPageText in ReplaceLoadedDocumentText pa prepišeta ujemajoče se bajte na mestu — pod pogojem, da je mogoče vsak nadomestni znak znova kodirati prek prvotne pisave, kar je fizična omejitev, ki jo ta članek obravnava pošteno, namesto da bi jo skril v opombo

Zahteva za to funkcijo je vedno vsakdanja. Podjetje se preimenuje, tri tisoč arhiviranih računov pa še vedno nosi staro ime. Predloga pogodbe je bila poslana z lanskim datumom poteka veljavnosti. Koda izdelka je bila umaknjena, vsak podatkovni list, ki jo omenja, pa namesto nje potrebuje kodo naslednika. V urejevalniku besedil je vsako od teh opravil tridesetsekundno delo. V PDF-ju pa je to resnično težka težava in razumevanje razlogov za to določa razliko med dobro uporabo API-ja in oddajo poročila o napaki, ki je v resnici le navajanje specifikacije

Zakaj je zamenjava besedila v PDF-ju tako težka?

Zamenjava besedila v PDF-ju je težka, ker stran PDF ne vsebuje besedila, ki ga je mogoče urejati — vsebuje pozicionirane glife. V skladu z modelom prikazovanja besedila iz ISO 32000-1 §9.4 tok vsebine poganja operaterje, kot sta Tj in TJ, ki izrisujejo zaporedja kod znakov na koordinatah, določenih z matriko besedila. Te kode niso Unicode; so indeksi v kodiranje, ki ga deklarira pisava strani, preslikava nazaj v čitljive znake pa lahko živi v tabeli /ToUnicode CMap, polju razlik kodiranja ali verigi preslikav CID. Ni objekta odstavka, ni toka besedila in ni nobenega zagotovila, da je ena vizualna beseda sploh shranjena kot en niz

Zamenjava doda drugo raven težavnosti na vrh dekodiranja: natančno morate vedeti, kateri bajti izvornega toka so ustvarili vsak glif, da lahko nove bajte vstavite natanko v to območje in nikamor drugam. Ekstraktor besedila si lahko privošči, da zavrže položaje bajtov, ko enkrat pridobi Unicode. Replacer pa ne. Zato je HotPDF delo razdelil na dve izdaji — v2.251.0 je zgradil plast sledenja odmikom in iskanja, v2.252.0 pa je na vrhu zgradil plast prepisovanja

Iskanje besedila: iskanje na ravni glifov s sledenjem bajtnim odmikom

Klic SearchLoadedDocumentText v HotPDF-ju najde vsako pojavitev iskanega niza z ujemanjem z dekodiranim zaporedjem glifov Unicode vsake strani in ne s surovimi bajti toka, zato je zadetek zadetek ne glede na to, kako ga je pisava kodirala. Infrastruktura pod tem je bila uvedena v v2.251.0: tokenizer toka vsebine zabeleži bajtni obseg StartOfs/EndOfs za vsak operand niza — vključno z njegovimi ločili ( ) ali < > — in vsak dekodiran glif nosi trojico TokenIndex/ItemIndex/ByteOffset, ki kaže nazaj na natančen operand, element polja TJ in kodno enoto, ki jo je ustvarila. Isti interpreter glifov poganja API za ekstrakcijo, opisan v ekstrakciji besedila iz naloženega PDF-ja v Delphiju; iskanje preprosto obdrži izvor, ki ga ekstrakcija zavrže

Vsako ujemanje se vrne kot zapis THPDFTextMatch, ki vsebuje indeks strani, vključujoč obseg glifov, X/Y izhodišče in širino zadetka v uporabniškem prostoru, izvorni žeton (token) in indeks elementa ter ujemajoče se besedilo samo. To je dovolj za izvedbo prekrivanja poudarkov, uporabniškega vmesnika za pregled ali koraka zamenjave. Iskanje, ki ne najde ničesar, vrne prazno matriko namesto napake, zato vzorec klicanja ostaja preprost

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;

Ena namerna odločitev pri zasnovi si zasluži opombo. Ko je CaseSensitive nastavljen na False, primerjava spreminja velikost le za znake ASCII. To je namerno: popolno spreminjanje velikosti znakov Unicode se obnaša drugače v orodjih od Delphi 5 do XE, ki jih HotPDF podpira, iskalni API, ki najde različne zadetke glede na to, kateri prevajalnik je zgradil vašo aplikacijo, pa je slabši od tistega z dokumentirano in predvidljivo mejo. Za latinsko poslovno besedilo — imena, kode, datume — spreminjanje velikosti ASCII pokriva praktične primere

Zamenjava besedila: obratno kodiranje in kirurško spajanje

Klic ReplaceLoadedDocumentText, dodan v HotPDF v2.252.0, prepiše vsako pojavitev iskanega niza z obratnim izvajanjem dekodirnega mehanizma. Funkcija HPDFEncodeUnicode je obratna funkcija dekoderja znakovnih kod: poteka po isti verigi strategij v obratni smeri — iskanje bfchar in bfrange v tabeli /ToUnicode, preslikava CID v toku kodiranja, preslikave identitete Type0 ter vnaprej določene tabele WinAnsi in MacRoman —, da vsak nadomestni znak spremeni nazaj v bajte znakovne kode, ki jih prvotna pisava pričakuje. Ponovno kodirani bajti se nato serializirajo v pravilno oblikovan dobesedni niz ali heksadecimalni niz, kar zrcali lastna pravila za izogibanje tokenizerja, tako da je cikel analiza → ponovna serializacija stabilen

Samo spajanje je kirurško in ne veleprodajno. Znotraj operanda niza se zamenja le obseg bajtov kode, ki ga pokriva ujemanje; neujemajoči se bajti v istem operandu, presledki med žetoni in vsak okoliški operater se ohranijo dobesedno, bajt za bajtom. Zamenjava bca znotraj abcabc prinaša a + zamenjava + bc in ne uničenega operanda. Zamenjave so lahko krajše ali daljše od iskanega niza — dobesedni niz se ponovno serializira, tok /Length pa se osveži —, vsak tok /Contents na večtokovni strani pa se obdela ločeno, tako da stran ostane pravilno oblikovana

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;

Upoštevajte, česa API ne počne: ne spreminja stavka strani. PDF nima preloma besedila, zato bo zamenjava, ki je vizualno širša od izvirnika, preprosto zasedla več vodoravnega prostora in lahko prekrije tisto, kar je bilo naslikano na njeni desni. Zamenjave enake ali podobne dolžine — datumi, nizi različic, številke delov, popravki imen — so idealne. Splošno preoblikovanje spada v izvorni dokument, ne pa v PDF

Zakaj ne morete zamenjati besedila z znaki, ki jih podnabor pisave nikoli ni vključeval?

Besedila ne morete zamenjati z znakom, ki ga vgrajeni podnabor pisave nikoli ni vključeval, ker zaporedje bajtov, ki bi izbralo ta znak, preprosto ne obstaja v tabelah preslikav pisave. Ko ustvarjalec PDF vgradi podnabor pisave, njena tabela /ToUnicode CMap in strukture kodiranja pokrivajo le tiste glife, ki jih je izvirni dokument dejansko uporabljal. Klic HPDFEncodeUnicode lahko obrne le preslikavo, ki je prisotna: če dokument v tej pisavi nikoli ni vseboval črke E, ni znakovne kode za E, ki bi jo lahko obrnili. To je fizična lastnost datoteke in ne omejitev katere koli knjižnice — nobeno orodje ne more pričarati preslikave glifov, ki nikoli ni bila vgrajena

HotPDF neuspeh rešuje konservativno. Če katerega koli posameznega znaka zamenjave ni mogoče znova kodirati, se celotna pojavitev iskanega niza preskoči — brez izjeme, brez delno popačenega besedila, pojavitev pa se preprosto ne šteje v ReplaceCount. Praktična posledica: primerjajte ReplaceCount s številom ujemanja iz predhodnega iskanja in morebitno razliko obravnavajte kot opozorilo. V zgornjem primeru z datumom se mora številka 6 nekje v besedilu dokumenta pojaviti v tej isti pisavi, da bi prepis uspel — kar je verjetno v računu, na splošno pa nikoli zagotovljeno. Ko znaki, ki jih potrebujete, preprosto niso na voljo, cilj pa je odstraniti občutljivo besedilo in ne njegovo preoblikovanje, je pravo odstranjevanje vsebine vseeno boljše orodje; glejte redakcijo in prestrukturiranje naloženih PDF-jev v Delphiju za to pot

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;

Drugi pogoj za preskok v tem sporočilu je druga dokumentirana meja: iskani niz, ki se razteza čez več operandov niza — npr. Hello, razdeljen med elementa [(He)(llo)] TJ —, se z iskanjem najde, ker se iskanje ujema z dekodiranim zaporedjem glifov, vendar se z zamenjavo preskoči, ker bi prepisovanje čez meje operandov zahtevalo združevanje sosednjih obsegov bajtov. Iskanje in nato preverjanje naredi obe omejitvi vidni namesto skriti

Kaj se spremeni v datoteki, ko jo shranite?

Zamenjan tok /Contents se shrani nestisnjen. S FlateDecode stisnjeni tokovi se za urejanje dekomprimirajo, ko pa HotPDF zapiše zgrajene bajte, opusti vnos toka /Filter in osveži /Length, namesto da bi jih znova stisnil. Rezultirajoči PDF je popolnoma veljaven in se normalno prikazuje v glavnih bralnikih; menjava je večja datoteka za vsak urejen tok. Za paketni cevovod, ki obdeluje na tisoče dokumentov, načrtujte to rast ali zaženite ločen korak stiskanja v nadaljnjem poteku. Kako spremenjeni objekti ob shranjevanju delujejo s strukturo navzkrižnih sklicev dokumenta, je lastna tema, obravnavana v tokovih objektov in inkrementalnih posodobitvah v HotPDF

Vse ostalo v datoteki ostane nedotaknjeno. Nespremenjeni tokovi ohranijo svoje stiskanje, pisave in slike se ne prepišejo, spajanje na ravni operanda pa pomeni, da se celo urejeni tokovi razlikujejo od izvirnika le tam, kjer je prišlo do ujemanja. Ta konservativnost je namerna: več ko naloženega dokumenta knjižnica prepiše, več možnosti ima, da zlomi kakšno posebnost ustvarjalca, ki je ni predvidela

Iskanje in zamenjava besedila se pridružujeta ekstrakciji, redakciji in upodabljanju strani v orodjarni naloženih dokumentov HotPDF, vse pa poganja isti interpreter toka vsebine in je na voljo od Delphi 5 do trenutnih izdaj RAD Studio brez zunanjih odvisnosti. Celotna referenca API in prenos preizkusne različice sta na strani izdelka HotPDF Component