HotPDF обходит каждый оператор потока содержимого страницы и передаёт каждый из них вашему коду через VisitLoadedPageContentOperators. Для каждого оператора вы выбираете действие: сохранить его, удалить, заменить собственными байтами, остановить обход или отклонить всю правку целиком. Если сохранены все операторы, переписанный поток побайтово идентичен исходному — именно это свойство делает механизм безопасным для запуска над документами, которые создавали не вы
Редактирование потоков содержимого — это как раз то место, где инструменты для PDF обычно ошибаются. Поток — это последовательность операторов над стековой машиной, а не дерево документа, и наивный подход с регулярным выражением по декодированным байтам ломается в тот момент, когда строковый литерал содержит что-то похожее на оператор, или встроенное изображение содержит произвольные бинарные данные. У посетителя, уже токенизировавшего поток, такой проблемы нет
Что реально можно сделать, имея доступ на уровне операторов?
Три семейства задач. Аудит: подсчитать, сколько раз страница задаёт цвет за пределами известного набора, или найти текст, нарисованный шрифтом, который не встроен. Удаление: убрать водяной знак, нарисованный как маркированное содержимое, или вырезать конкретный вызов изображения. И нормализация: заменить одну форму оператора на эквивалентную, которую лучше обрабатывает нижестоящий потребитель
У каждой из этих задач одно общее требование: вы должны иметь возможность оставить нетронутыми остальные 99 процентов потока. Дерево синтаксиса HotPDF хранит одну копию исходника плюс компактные диапазоны, а правки — это отсортированные неперекрывающиеся оверлеи. Запись результата копирует нетронутые диапазоны напрямую и выдаёт байты замены только там, где вы что-то изменили, поэтому проход без изменений не может внести расхождения
Контракт посетителя
Реализуйте интерфейс или назначьте обработчик события, затем вызовите метод обхода с ApplyChanges, установленным в false для аудита или в true для перезаписи. Данные события несут фазу, имя оператора, число его операндов, его байтовый диапазон, текущее состояние вложенности и заимствованное представление для чтения операндов:
uses
HPDFDoc, HPDFContentStream;
procedure TAuditor.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
var
Bytes: TBytes;
begin
if EventData.Phase <> covpOperator then
Exit;
// Считаем операторы отображения текста, нарисованные внутри маркированного содержимого
if (EventData.OperatorName = 'Tj') or
(EventData.OperatorName = 'TJ') then
begin
Inc(FTextOperators);
if EventData.State.MarkedContentDepth > 0 then
Inc(FTextInsideMarkedContent);
end;
// Читаем операнд только тогда, когда он действительно нужен
if EventData.OperatorName = 'Do' then
if EventData.View.GetOperandBytes(0, Bytes) then
NoteXObjectUse(TEncoding.ANSI.GetString(Bytes));
EventData.Action := coaKeep;
end;
procedure TAuditor.Run(const FileName: string);
var
Pdf: THotPDF;
Stats: THPDFContentOperatorVisitStatistics;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile(FileName) <= 0 then
Exit;
Pdf.OnContentOperator := HandleOperator;
for I := 0 to Pdf.LoadedPageCount - 1 do
Pdf.VisitLoadedPageContentOperators(I, False, Stats); // только аудит
Writeln(Format('%d operators visited, max q/Q depth %d',
[Stats.VisitedOperatorCount, Stats.MaximumGraphicsStateDepth]));
finally
Pdf.Free;
end;
end;
Почему представление заимствованное, а не изменяемое?
Обратный вызов никогда не получает изменяемое дерево синтаксиса. Он получает представление, способное по запросу вернуть байты текущей операции или конкретный операнд, и это всё. Любое изменение проходит через поля действия и замены
Это разделение — не церемония. Прямая мутация дерева позволила бы посетителю обойти бюджет замены и проверку на перекрывающиеся правки, а перекрывающиеся правки в модели оверлеев по байтовым диапазонам дают вывод, который молча оказывается неверным, а не громко ломается. Маршрутизация изменений через поле действия означает, что каждая правка проверяется одинаково независимо от намерения посетителя
Чтение по запросу также удерживает типичный случай дешёвым. Аудит, который лишь считает имена операторов, вообще никогда не материализует байты операндов, а это важно для потока содержимого с сотнями тысяч операций
Поле состояния всегда описывает вход
Каждое событие несёт глубину состояния графики, глубину текстового объекта, глубину маркированного содержимого и глубину секции совместимости такими, какими они были в исходном входе непосредственно перед выполнением этого оператора. Это верно, даже если ваш посетитель удалил или заменил более ранние операторы
Привязка состояния к исходному входу, а не к отредактированному выводу, — осознанный выбор с практической выгодой: повторный запуск того же аудита над тем же документом даёт идентичные результаты, а аудит, выполняемый одновременно с правками, не смещает собственную систему отсчёта. Если вам нужно знать глубину в выводе, вы знаете, что удалили, и можете её вычислить; обратное — восстановление устойчивых входных позиций из движущегося вывода — невозможно
Удаление и замена, с правилами, которые вас защищают
Замена — это сырые байты потока содержимого, и вы отвечаете за их синтаксис. Защитные механизмы вокруг них — это бюджет на общее число байт замены, требование непересечения и опциональная проверка вывода, повторно разбирающая результат перед его принятием:
procedure TWatermarkStripper.HandleOperator(Sender: TObject;
var EventData: THPDFContentOperatorEventData);
begin
EventData.Action := coaKeep;
if EventData.Phase <> covpOperator then
Exit;
// Убираем операторы, рисующие форму водяного знака
if (EventData.OperatorName = 'Do') and OperandIsWatermark(EventData) then
EventData.Action := coaDelete
// Нормализуем случайный цвет до фирменного серого
else if EventData.OperatorName = 'rg' then
begin
EventData.Replacement := TEncoding.ANSI.GetBytes('0.2 0.2 0.2 rg');
EventData.Action := coaReplace;
end
// Отклоняем всю правку, если встретили что-то непонятное
else if EventData.OperatorName = 'BI' then
EventData.Action := coaReject;
end;
var
Options: THPDFContentOperatorVisitOptions;
begin
Options := THPDFContentOperatorVisitOptions.Default;
Options.ValidateOutput := True; // повторный разбор перед принятием
Options.MaxReplacementBytes := 64 * 1024;
Pdf.OnContentOperator := HandleOperator;
if Pdf.VisitLoadedPageContentOperators(PageIndex, True, Options, nil, Stats) then
if Stats.Modified then
Pdf.SaveLoadedDocument('cleaned.pdf');
end;
Набор действий неспроста включает два выхода. coaStop завершает обход и сохраняет то, что решено на данный момент, — это правильно, когда вы нашли то, что искали. coaReject отменяет весь обход целиком и оставляет страницу нетронутой — это правильно, когда вам встречается конструкция, не покрытая вашими правилами перезаписи. Классический пример — встроенные изображения: их бинарную полезную нагрузку не должен редактировать посетитель, нормализующий цвета
Чтение статистики и где останавливается обход
Поле статуса отличает завершение от остановки, отказа, недопустимого входа, превышения лимита, недопустимого вывода и ошибки посетителя. Это разные проблемы: недопустимый вывод означает, что ваши байты замены не прошли повторный разбор, а недопустимый вход означает, что исходный поток уже был некорректен и правка не предпринималась
Счётчики делают аудит самопроверяемым. VisitedOperatorCount должен сходиться с суммой KeptOperatorCount, DeletedOperatorCount и ReplacedOperatorCount, а цифры максимальной глубины подсказывают, достаточно ли глубоко вложена страница, чтобы стоило присмотреться внимательнее. SourceBytes в сравнении с OutputBytes даёт эффект вашей правки на размер одним числом
Для типичных задач есть маршруты более высокого уровня, вообще не требующие написания посетителя. Постоянное удаление содержимого с очисткой ресурсов, которая нужна настоящей редакции, описано в статье редактирование с удалением содержимого и сшивка страниц загруженных документов. Замена видимого текста описана в статье поиск и замена текста в существующем PDF. Обращайтесь к посетителю операторов, когда ваше правило структурное, а не текстовое, или когда вам нужен аудит, на который не отвечает ни один существующий отчёт
Обход, извлечение и перезапись потоков содержимого работают с одной и той же моделью загруженного документа в Delphi и C++Builder; полный список возможностей — на странице компонента HotPDF Delphi PDF Component