Komponenta HotPDF Component dokáže vyhledávat a nahrazovat text uvnitř existujícího PDF z Delphi a C++Builderu. Metody SearchLoadedPageText a SearchLoadedDocumentText vyhledávají každý výskyt řetězce s přesností na úroveň glyfů a ReplaceLoadedPageText a ReplaceLoadedDocumentText přepisují odpovídající bajty přímo na místě — za předpokladu, že každý nahrazovaný znak lze znovu zakódovat prostřednictvím původního písma, což je fyzické omezení, kterým se tento článek zabývá upřímně, spíše než by ho skrýval v poznámce pod čarou
Požadavek stojící za touto funkcí bývá obvykle všední. Firma se přejmenuje a tři tisíce archivovaných faktur stále nese starý název. Šablona smlouvy byla odeslána s loňským datem vypršení platnosti. Kód produktu byl vyřazen a každý technický list, který jej zmiňuje, potřebuje namísto něj kód nástupce. V textovém procesoru je každá z těchto úloh záležitostí na třicet sekund. V PDF se však jedná o skutečně těžký problém a pochopení důvodů představuje rozdíl mezi správným používáním API a nahlášením chyby, která je ve skutečnosti citací specifikace
Proč je nahrazení textu v PDF tak těžké?
Nahrazení textu v PDF je obtížné, protože stránka PDF neobsahuje upravitelný text — obsahuje umístěné glyfy. Podle modelu zobrazování textu v normě ISO 32000-1 §9.4 řídí stream obsahu operátory jako Tj a TJ, které kreslí sekvence kódů znaků na souřadnicích určených textovou maticí. Tyto kódy nejsou Unicode; jsou to indexy do jakéhokoli kódování, které definuje písmo stránky, a mapování zpět na čitelné znaky může být uloženo v tabulce /ToUnicode CMap, poli rozdílů kódování nebo řetězci mapování CID. Neexistuje žádný objekt odstavce, žádný tok textu a žádná záruka, že jedno vizuální slovo je vůbec uloženo jako jeden řetězec
Nahrazení přidává k dekódování druhou úroveň obtížnosti: musíte přesně vědět, které bajty původního streamu vytvořily každý glyf, abyste mohli nové bajty vložit přesně do tohoto rozsahu a nikam jinam. Extraktor textu si může dovolit zahodit pozice bajtů, jakmile získá Unicode výstup. Nástroj pro nahrazování však nikoli. Proto knihovna HotPDF rozdělila tuto práci do dvou verzí — verze v2.251.0 vybudovala vrstvu sledování offsetů a vyhledávání a verze v2.252.0 na ní postavila vrstvu přepisu
Hledání textu: vyhledávání na úrovni glyfů se sledováním bajtového offsetu
Metoda SearchLoadedDocumentText v HotPDF vyhledává každý výskyt hledaného textu porovnáváním s dekódovanou sekvencí glyfů Unicode každé stránky, nikoli s čistými bajty streamu, takže shoda je shodou bez ohledu na to, jak ji písmo zakódovalo. Podkladová infrastruktura byla představena ve verzi v2.251.0: tokenizer streamu obsahu zaznamenává rozsah bajtů StartOfs/EndOfs pro každý řetězcový operand — včetně jeho omezovačů ( ) nebo < > — a každý dekódovaný glyf nese trojici TokenIndex/ItemIndex/ByteOffset ukazující zpět na přesný operand, položku pole TJ a jednotku kódu, která jej vytvořila. Stejný interpret glyfů pohání API pro extrakci popsané v extrakci textu z načteného PDF v Delphi; vyhledávání jednoduše zachovává původ, který extrakce zahazuje
Každá shoda se vrací jako záznam THPDFTextMatch nesoucí index stránky, inkluzivní rozsah glyfů, počátek X/Y v uživatelském prostoru a šířku shody, zdrojový token a index položky a samotný shodný text. To stačí k provedení překrytí zvýrazněním, uživatelského rozhraní pro kontrolu nebo kroku nahrazení. Vyhledávání, které nic nenajde, vrací prázdné pole namísto selhání, takže vzorec volání zůstává jednoduchý
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;
Jedna záměrná volba návrhu stojí za zmínku. Pokud je CaseSensitive nastaveno na False, porovnávání převádí velikost písmen pouze u znaků ASCII, a to záměrně: plný převod velikosti písmen v kódování Unicode se chová odlišně napříč verzemi Delphi 5 až XE, které HotPDF podporuje, a vyhledávací API, které nachází různé shody v závislosti na tom, který kompilátor vaši aplikaci sestavil, je horší než to s dokumentovaným a předvídatelným limitem. Pro latinský obchodní text — jména, kódy, data — ASCII převod pokrývá praktické případy
Nahrazování textu: reverzní kódování a chirurgické spojování
Metoda ReplaceLoadedDocumentText, přidaná v HotPDF verze v2.252.0, přepisuje každý výskyt hledaného textu spuštěním dekódovacího mechanismu pozpátku. Funkce HPDFEncodeUnicode je inverzí k dekodéru znakových kódů: prochází stejný řetězec strategií v opačném pořadí — vyhledávání /ToUnicode bfchar a bfrange, mapování CID streamu kódování, mapování identity Type0 a předdefinované tabulky WinAnsi a MacRoman — aby převedla každý nahrazovaný znak zpět na bajty znakového kódu, které původní písmo očekává. Znovu zakódované bajty jsou poté serializovány do správně formátovaného řetězcového literálu nebo hexadecimálního řetězce, což kopíruje pravidla pro escape znaky tokenizeru, aby byl cyklus analýza → opětovná serializace stabilní
Samotné spojení je chirurgické a nikoli plošné. Uvnitř řetězcového operandu se nahrazuje pouze rozsah bajtů kódu pokrytý shodou; neshodující se bajty ve stejném operandu, prázdné znaky mezi tokeny a každý okolní operátor jsou zachovány doslova, bajt po bajtu. Nahrazení bca uvnitř abcabc dává a + nahrazení + bc, nikoli poškozený operand. Nahrazení mohou být kratší nebo delší než hledaný text — literál je znovu serializován a hodnota /Length streamu je aktualizována — a každý stream /Contents na stránce s více streamy se zpracovává izolovaně, aby stránka zůstala správně formátovaná
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;
Všimněte si, co toto API nedělá: nepřesazuje stránku. PDF nemá žádné zalamování (reflow), takže nahrazení, které je vizuálně širší než originál, jednoduše zabere více vodorovného prostoru a může stlačit cokoli, co bylo vykresleno po jeho pravé straně. Výměny stejné délky nebo blízké délky — data, řetězce verzí, čísla dílů, opravy jmen — jsou ideálním případem použití. Rozsáhlé změny textu patří do zdrojového dokumentu, nikoli do PDF
Proč nelze nahradit text znaky, které podsada písma nikdy neobsahovala?
Nemůžete nahradit text znakem, který vložená podsada písma nikdy neobsahovala, protože sekvence bajtů, která by tento znak vybrala, v mapovacích tabulkách písma prostě neexistuje. Když tvůrce PDF vloží písmo s podsadou, jeho tabulka /ToUnicode CMap a struktury kódování pokrývají pouze glyfy, které původní dokument skutečně používal. Funkce HPDFEncodeUnicode může vrátit pouze mapování, které je přítomno: pokud dokument v tomto písmu nikdy neobsahoval písmeno E, neexistuje žádný kód znaku pro E, na který by se dalo provést reverzní kódování. Jedná se o fyzickou vlastnost souboru, nikoli o omezení konkrétní knihovny — žádný nástroj nedokáže vykouzlit mapování glyfů, které nebylo nikdy vloženo
Knihovna HotPDF řeší selhání konzervativně. Pokud nelze znovu zakódovat byť jen jediný znak nahrazení, celý tento výskyt hledaného textu se přeskočí — žádná výjimka, žádný částečně poškozený text a výskyt se jednoduše nezapočítá do ReplaceCount. Praktický důsledek: porovnejte ReplaceCount s počtem shod z předchozího vyhledávání a berte rozdíl jako signál. Ve výše uvedeném příkladu s datem se musí číslice 6 někde v textu dokumentu ve stejném písmu vyskytovat, aby přepis uspěl — což je pravděpodobné na faktuře, ale obecně to není nikdy zaručeno. Pokud potřebné znaky prostě nejsou k dispozici a cílem je odstranit citlivý text spíše než jej přepsat, je beztak lepším nástrojem skutečné odstranění obsahu; viz anonymizace a restrukturalizace načtených PDF v Delphi pro tuto cestu
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;
Druhá podmínka přeskočení v této zprávě je další dokumentovanou hranicí: hledaný text, který se rozprostírá přes více řetězcových operandů — například Hello rozdělené mezi položky [(He)(llo)] TJ — je vyhledáváním nalezen, protože vyhledávání porovnává dekódovanou sekvenci glyfů, ale při nahrazování je přeskočen, protože přepis přes hranice operandů by vyžadoval sloučení sousedních rozsahů bajtů. Postup vyhledat-a-ověřit činí oba tyto limity viditelnými, nikoli tichými
Co se v souboru změní při uložení?
Nahrazený stream /Contents se ukládá nekomprimovaný. Streamy komprimované pomocí FlateDecode se pro úpravy dekomprimují, a když HotPDF zapisuje přestavěné bajty, odstraní položku /Filter streamu a aktualizuje /Length namísto opětovné komprese. Výsledné PDF je plně platné a vykresluje se normálně v běžných prohlížečích; kompromisem je větší soubor pro každý upravený stream. U dávkové linky zpracovávající tisíce dokumentů počítejte s tímto nárůstem nebo spusťte samostatný kompresní průchod dále v řetězci. Jak přepsané objekty při uložení interagují se strukturou křížových odkazů dokumentu, je samostatné téma, kterému se věnuje článek o objektových streamech a inkrementálních aktualizacích v HotPDF
Vše ostatní v souboru zůstává nedotčeno. Nedotčené streamy si zachovávají svou kompresi, písma a obrázky se nepřepisují a spojení na úrovni operandů znamená, že se i upravené streamy liší od originálu pouze tam, kde přistála shoda. Tento konzervatismus je záměrný: čím více z načteného dokumentu knihovna přepisuje, tím více příležitostí má k narušení nějaké zvláštnosti tvůrce, kterou nepředvídala
Vyhledávání a nahrazování textu doplňuje extrakci, anonymizaci a vykreslování stránek v sadě nástrojů HotPDF pro načtené dokumenty. Vše je řízeno stejným interpretem streamu obsahu a je k dispozici od verze Delphi 5 až po aktuální verze RAD Studio bez externích závislostí. Kompletní referenční příručka k API a zkušební verze ke stažení jsou na stránce produktu HotPDF Component