HotPDF Component може виконувати пошук та заміну тексту всередині існуючого PDF з Delphi та C++Builder. SearchLoadedPageText та SearchLoadedDocumentText знаходять кожне входження рядка з точністю до гліфа, а ReplaceLoadedPageText та ReplaceLoadedDocumentText перезаписують знайдені байти на місці — за умови, що кожен символ заміни може бути закодований через оригінальний шрифт (фізичне обмеження, яке в цій статті розглядається чесно, а не ховається у виносках)
Запит на цю функцію завжди прозаїчний. Компанія змінює назву, а три тисячі архівних рахунків-фактур все ще містять стару назву. Шаблон договору був випущений з минулорічною датою закінчення терміну дії. Код продукту було знято з виробництва, і в кожному технічному паспорті, де він згадується, замість нього потрібен код наступника. У текстовому процесорі кожне з цих завдань займає тридцять секунд. У PDF це справді складна проблема, і розуміння причин цього визначає різницю між правильним використанням API та подачею звіту про помилку, яка насправді є посиланням на специфікацію
Why is replacing text in a PDF so hard?
Заміна тексту в PDF є складною, оскільки сторінка PDF не містить редагованого тексту — вона містить позиціоновані гліфи. Відповідно до моделі відображення тексту стандарту ISO 32000-1 §9.4, потік вмісту керує операторами на кшталт Tj та TJ, які малюють послідовності кодів символів у координатах, встановлених текстовою матрицею. Ці коди не є Unicode; вони є індексами у кодуванні, яке оголошує шрифт сторінки, а відображення назад на читабельні символи може міститися у CMap /ToUnicode, масиві різниць кодування або ланцюжку відображення CID. У PDF немає об'єкта абзацу, немає перетікання тексту і немає гарантії, що одне візуальне слово взагалі зберігається як один рядок
Заміна додає другий рівень складності поверх декодування: ви повинні точно знати, які саме байти вихідного потоку створили кожен гліф, щоб мати змогу вставити нові байти саме в цей проміжок і нікуди більше. Екстрактор тексту може дозволити собі відкинути позиції байтів, щойно він отримає Unicode. Програма заміни цього зробити не може. Саме тому HotPDF розділив роботу між двома випусками — версія v2.251.0 створила шар відстеження зсувів та пошуку, а версія v2.252.0 побудувала шар перезапису поверх нього
Finding text: glyph-level search with byte-offset tracking
Метод SearchLoadedDocumentText у HotPDF знаходить кожне входження шуканого слова шляхом зіставлення з декодованою послідовністю гліфів Unicode кожної сторінки, а не з сирими байтами потоку, тому збіг є точним незалежно від того, як шрифт його закодував. Базова інфраструктура була представлена у версії v2.251.0: токенізатор потоку вмісту фіксує діапазон байтів StartOfs/EndOfs для кожного рядкового операнда — включаючи його обмежувачі ( ) або < > — і кожен декодований гліф містить трійку TokenIndex/ItemIndex/ByteOffset, яка вказує на точний операнд, елемент масиву TJ та одиницю коду, що його створили. Той самий інтерпретатор гліфів керує API вилучення, описаним у вилученні тексту із завантаженого PDF у Delphi; пошук просто зберігає походження, яке вилучення відкидає
Кожен збіг повертається як запис THPDFTextMatch, що містить індекс сторінки, включний діапазон гліфів, координати X/Y початку координат та ширину збігу в просторі користувача, індекс вихідного токена та елемента, а також сам знайдений текст. Цього достатньо для накладання виділення, інтерфейсу рецензування або кроку заміни. Пошук, який нічого не знайшов, повертає порожній масив замість помилки, тому шаблон виклику залишається простим
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;
Одне свідоме дизайнерське рішення заслуговує на увагу. Коли параметр CaseSensitive має значення False, порівняння згортає регістр лише для символів ASCII: повне згортання регістру Unicode поводиться по-різному в інструментаріях від Delphi 5 до XE, які підтримує HotPDF, і API пошуку, який знаходить різні збіги залежно від того, який компілятор створив ваш додаток, є гіршим, ніж API з документованим, передбачуваним обмеженням. Для латинського бізнес-тексту — імен, кодів, дат — згортання ASCII охоплює всі практичні випадки
Replacing text: reverse encoding and surgical splicing
Метод ReplaceLoadedDocumentText, доданий у HotPDF версії v2.252.0, перезаписує кожне входження шуканого слова шляхом запуску механізму декодування у зворотному напрямку. Функція HPDFEncodeUnicode є зворотною до декодера кодів символів: вона проходить той самий ланцюжок стратегій у зворотному напрямку — пошук у bfchar та bfrange /ToUnicode, відображення CID потоку кодування, відображення ідентичності Type0 та зумовлені таблиці WinAnsi та MacRoman — щоб перетворити кожен символ заміни назад у байти коду символу, які очікує оригінальний шрифт. Потім повторно закодовані байти серіалізуються у коректний рядковий літерал або шістнадцятковий рядок, відображаючи власні правила екранування токенізатора, щоб цикл «розбір → повторна серіалізація» був стабільним
Саме зрощення є хірургічним, а не масовим. Усередині рядкового операнда замінюється лише діапазон байтів коду, охоплений збігом; незбіжні байти в тому самому операнді, пробіли між токенами та кожен навколишній оператор зберігаються дослівно, байт за байтом. Заміна bca всередині abcabc дає a + заміна + bc, а не пошкоджений операнд. Заміни можуть бути коротшими або довшими за шукане слово — літерал повторно серіалізується, а /Length потоку оновлюється — і кожен потік /Contents багатопотокової сторінки обробляється ізольовано, щоб сторінка залишалася коректною
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;
Зверніть увагу, чого цей API не робить: він не виконує повторну верстку сторінки. У PDF немає перетікання тексту, тому заміна, яка візуально ширша за оригінал, просто займе більше горизонтального простору і може перекрити те, що було намальовано праворуч від неї. Заміни однакової або близької довжини — дати, рядки версій, номери деталей, виправлення імен — є ідеальною областю застосування. Масове переформулювання має відбуватися у вихідному документі, а не в PDF
Why can't you replace text with characters the font subset never included?
Ви не можете замінити текст символом, який вбудована підмножина шрифту ніколи не містила, оскільки послідовність байтів, яка б вибрала цей символ, просто відсутня в таблицях відображення шрифту. Коли генератор PDF вбудовує субсетований шрифт, його CMap /ToUnicode та структури кодування охоплюють лише ті гліфи, які фактично використовувалися в оригінальному документі. HPDFEncodeUnicode може повернути назад лише те відображення, яке присутнє: якщо документ ніколи не містив літеру E у цьому шрифті, немає коду символу, до якого можна було б повернути E. Це фізична властивість файлу, а не обмеження якоїсь конкретної бібліотеки — жоден інструмент не може створити відображення гліфів, яке ніколи не було вбудоване
HotPDF обробляє відмову консервативно. Якщо будь-який один символ заміни не може бути закодований повторно, все це входження шуканого слова пропускається — без винятку, без частково спотвореного тексту, і це входження просто не враховується в ReplaceCount. Практичний наслідок: перевірте ReplaceCount проти кількості збігів з попереднього пошуку і сприймайте невідповідність як сигнал. У наведеному вище прикладі з датою цифра 6 повинна з'являтися десь у тексті документа тим самим шрифтом, щоб перезапис був успішним — це ймовірно в рахунку-фактурі, але ніколи не гарантується в загальному випадку. Коли потрібні символи просто недоступні, а метою є видалення конфіденційного тексту, а не зміна формулювання, кращим інструментом є справжнє видалення вмісту; див. редагування та реструктуризація завантажених PDF у Delphi для цього шляху
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;
Друга умова пропуску в цьому повідомленні є іншою документованою межею: шукане слово, яке охоплює кілька рядкових операндів — наприклад, Hello, розділене між елементами [(He)(llo)] TJ — знаходиться пошуком, оскільки пошук зіставляє декодовану послідовність гліфів, но пропускається заміною, оскільки перезапис через межі операндів вимагав би об'єднання сусідніх діапазонів байтів. Пошук з наступною перевіркою робить обидва обмеження видимими, а не прихованими
What changes in the file when you save?
Замінений потік /Contents зберігається нестиснутим. Стиснуті за допомогою FlateDecode потоки декомпресуються для редагування, і коли HotPDF записує перебудовані байти, він відкидає запис /Filter потоку та оновлює /Length замість повторного стиснення. Отриманий PDF-файл є повністю коректним і рендериться нормально у основних переглядачах; компроміс полягає у збільшенні розміру файлу для кожного відредагованого потоку. Для конвеєра, який обробляє тисячі документів, передбачте це зростання або запустіть окремий прохід стиснення далі по конвеєру. Те, як перезаписані об'єкти взаємодіють із структурою перехресних посилань документа при збереженні, є окремою темою, яка розглядається в потоках об'єктів та інкрементних оновленнях в HotPDF
Все інше у файлі залишається недоторканим. Незмінені потоки зберігають своє стиснення, шрифти та зображення не перезаписуються, а зрощення на рівні операндів означає, що навіть відредаговані потоки відрізняються від оригіналу лише там, де ліг збіг. Цей консерватизм є навмисним: чим більшу частину завантаженого документа переписує бібліотека, тим більше можливостей вона має для порушення особливостей генератора, які вона не передбачила
Пошук та заміна тексту доповнюють інструментарій завантажених документів HotPDF для вилучення, редагування та рендерингу сторінок, причому все це працює на одному інтерпретаторі потоку вмісту та доступно від Delphi 5 до поточних випусків RAD Studio без зовнішніх залежностей. Повний довідник API та демо-версія доступні на сторінці продукту HotPDF Component