HotPDF Delphi Component умеет искать и заменять текст внутри существующего PDF из Delphi и C++Builder. SearchLoadedPageText и SearchLoadedDocumentText находят каждое вхождение строки с точностью до глифа, а ReplaceLoadedPageText и ReplaceLoadedDocumentText переписывают найденные байты на месте — при условии, что каждый символ замены можно закодировать обратно через исходный шрифт; это физическое ограничение статья разбирает честно, а не прячет в сноску
Запрос, стоящий за этой возможностью, всегда бытовой. Компания переименовалась, а три тысячи архивных счетов по-прежнему несут старое название. Шаблон договора ушёл с прошлогодней датой окончания. Код товара сняли с производства, и каждой спецификации, где он упомянут, нужен код-преемник. В текстовом процессоре любая из этих задач решается за тридцать секунд. В PDF это по-настоящему трудная проблема, и понимание причины отделяет грамотное использование API от заведения баг-репорта, который на деле является цитатой из спецификации
Почему заменить текст в PDF так трудно?
Заменить текст в PDF трудно потому, что страница PDF не содержит редактируемого текста — она содержит размещённые глифы. По модели вывода текста из ISO 32000-1 §9.4 поток содержимого управляет операторами вроде Tj и TJ, которые рисуют последовательности кодов символов в координатах, заданных матрицей текста. Эти коды — не Unicode; это индексы в той кодировке, которую объявляет шрифт страницы, а отображение обратно в читаемые символы может жить в CMap /ToUnicode, в массиве различий кодировки или в цепочке отображений CID. Нет ни объекта абзаца, ни потока текста, ни гарантии, что одно видимое слово вообще хранится как одна строка
Замена добавляет к декодированию второй слой сложности: нужно точно знать, какие байты исходного потока произвели каждый глиф, чтобы вставить новые байты ровно в этот отрезок и никуда больше. Экстрактор текста может позволить себе выбросить байтовые позиции, как только вытащит Unicode. Заменяющий код — не может. Именно поэтому HotPDF разделил работу на два выпуска: v2.251.0 построила слой отслеживания смещений и поиска, а v2.252.0 построила поверх него слой перезаписи
Поиск текста: поиск по глифам с отслеживанием байтовых смещений
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 хватает на практические случаи
Замена текста: обратная кодировка и хирургическая вставка
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
Почему нельзя заменить текст символами, которых урезанный шрифт никогда не включал?
Заменить текст символом, которого встроенный урезанный шрифт никогда не включал, нельзя потому, что байтовой последовательности, которая выбрала бы этот символ, попросту нет в таблицах отображения шрифта. Когда производитель 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, находится поиском, потому что поиск сопоставляет декодированную последовательность глифов, но пропускается заменой, потому что перезапись через границы операндов потребовала бы слияния соседних байтовых отрезков. Схема «сначала поиск, потом сверка» делает оба предела видимыми вместо молчаливых
Что меняется в файле при сохранении?
Заменённый поток /Contents сохраняется несжатым. Потоки, сжатые FlateDecode, распаковываются для правки, и когда HotPDF пишет пересобранные байты, он убирает запись /Filter потока и обновляет /Length, а не сжимает заново. Получающийся PDF полностью корректен и нормально отрисовывается в основных программах просмотра; плата — более крупный файл за каждый отредактированный поток. Для пакетного конвейера, обрабатывающего тысячи документов, заложите этот рост в расчёты или запустите отдельный проход сжатия ниже по течению. Как переписанные объекты взаимодействуют со структурой перекрёстных ссылок документа при сохранении — отдельная тема, раскрытая в статье потоки объектов и инкрементные обновления в HotPDF
Всё остальное в файле остаётся нетронутым. Незатронутые потоки сохраняют своё сжатие, шрифты и изображения не переписываются, а вставка на уровне операнда означает, что даже отредактированные потоки отличаются от оригинала только там, где легло совпадение. Такая консервативность намеренна: чем больше в загруженном документе библиотека переписывает, тем больше у неё возможностей сломать причуду производителя, которую она не предусмотрела
Поиск и замена текста присоединяются к извлечению, редактированию и отрисовке страниц в наборе инструментов HotPDF для загруженных документов, и все они управляются одним и тем же интерпретатором потока содержимого и доступны от Delphi 5 до текущих выпусков RAD Studio без внешних зависимостей. Полный справочник по API и пробная версия для загрузки находятся на странице продукта HotPDF Delphi Component