Вы вызываете AddText, чтобы проштамповать строку на странице PDF с PDFiumPas, затем сразу вызываете FindFirst, чтобы подтвердить, что штамп появился, и поиск возвращает пусто. Текст на странице есть — Acrobat его показывает, — но компонент TPdf в PDFiumPas хранит отдельную закэшированную структуру FPDF_TEXTPAGE, разобранную один раз из потока содержимого страницы, и правка сама по себе задним числом эту структуру не обновляет. Запросите её до того, как она была обновлена, и вы прочтёте страницу в точности такой, какой она выглядела до вашего изменения, а не после
Почему PDFium возвращает устаревший текст сразу после правки?
PDFiumPas оборачивает движок рендеринга PDFium от Google для Delphi и C++Builder, и его вызовы для текста и редактирования обращаются к двум разным подсистемам внутри этого движка. FPDF_TEXTPAGE принадлежит стороне чтения: FPDFText_LoadPage один раз обходит поток содержимого страницы и строит текстовую страницу — коды символов, позиции, метрики шрифтов, границы слов, — и PDFiumPas хранит эту структуру в кэше на протяжении всего времени, пока страница остаётся загруженной. Вызовы редактирования, такие как FPDFPage_InsertObject или FPDFPage_GenerateContent, работают с совершенно другим представлением — графом объектов и потока содержимого страницы, и PDFium не проталкивает эти изменения в уже открытую текстовую страницу самостоятельно. Перестройка её при каждой правке сделала бы пакетное редактирование неприемлемо медленным, так что конструкция вместо этого обменивает эту стоимость на правило: тот, кто держит дескриптор, закрывает его после правки, изменяющей содержимое, а следующее чтение строит свежий
Внутри текстового кэша TPdf: FTextPage, LoadTextPage и UnloadTextPage
TPdf отслеживает закэшированный дескриптор в одном приватном поле, FTextPage, и оборачивает его жизненный цикл в два метода. LoadTextPage проверяет, равен ли FTextPage nil, и только в этом случае вызывает FPDFText_LoadPage для текущей страницы; если дескриптор уже существует, LoadTextPage переиспользует его, не спрашивая, изменилась ли страница с момента его построения. UnloadTextPage — вторая половина: он закрывает нативный дескриптор через FPDFText_ClosePage, возвращает FTextPage обратно в nil, а также сбрасывает закэшированный список веб-ссылок и любую выполняющуюся сессию поиска, поскольку оба они были получены из той же текстовой страницы и устаревают по той же причине
Поведение LoadTextPage «переиспользовать без проверки» — именно причина, по которой важна последовательность действий. Каждый текстовый запрос к TPdf — Text, FindFirst, GetWebLinks — сначала направляется через LoadTextPage, так что пока FTextPage всё ещё держит дескриптор до правки, ни один из этих вызовов никак не может узнать, что изменение произошло. Навигация по страницам никогда не была здесь риском: UnloadPage, выполняющийся при переключении страниц, перезагрузке и закрытии документа, всегда закрывал текстовую страницу вместе с самой страницей. Открытым вопросом всегда были правки, применённые к странице, на которой вы всё ещё находитесь
Какие методы PDFiumPas обновляют кэш автоматически?
Собственные методы редактирования страниц у TPdf — AddText, SetText, SetTextPositions, AddPath, RemoveObject и InsertFormObjectFromXObject — каждый вызывает UnloadTextPage перед вызовом UpdatePage (FPDFPage_GenerateContent из PDFium) для сериализации изменения в поток содержимого. Вызовите любой из них, и самый следующий вызов Text, FindFirst или GetWebLinks перестроит текстовую страницу из содержимого в том виде, в каком оно сейчас находится, без какого-либо дополнительного вызова с вашей стороны
var
Pdf: TPdf;
Index: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'invoice.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
Pdf.AddText('Reviewed by J. Alvarez', 'Helvetica', 10, 72, 40, clBlack, 255, 0);
// AddText already closed the cached text page, so this FindFirst
// call rebuilds it fresh before it searches
Index := Pdf.FindFirst('Reviewed by J. Alvarez');
if Index >= 0 then
ShowMessage('Stamp confirmed at character ' + IntToStr(Index));
finally
Pdf.Free;
end;
end;
Паттерн, который всё ещё ломается: кэширование сырого дескриптора TextPage
TPdf предоставляет живой дескриптор через свойство только для чтения TextPage, для редкого случая, когда вам нужно вызвать функцию FPDFText_*, которую PDFiumPas ещё не обернул. Этот аварийный выход — как раз то единственное место, где автоматическая инвалидация помочь не может: как только вы копируете значение FPDF_TEXTPAGE из свойства в локальную переменную, у PDFiumPas нет способа узнать, что вы всё ещё держите его, и нет способа обновить вашу копию, когда UnloadTextPage выполнится где-то ещё в вашем коде
var
Pdf: TPdf;
RawHandle: FPDF_TEXTPAGE;
StaleCount: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'contract.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
RawHandle := Pdf.TextPage; // FPDFText_LoadPage handle, cached in FTextPage
Pdf.SetText(0, 'Amended Clause 4.2');
// SetText already closed RawHandle and set Pdf.TextPage back to nil.
// Calling any FPDFText_* function against the old value now touches a
// handle PDFium has already freed — undefined behavior, not a bug you
// can catch with a nil check
StaleCount := FPDFText_CountChars(RawHandle);
finally
Pdf.Free;
end;
end;
Использование дескриптора после того, как на нём был выполнен FPDFText_ClosePage, — неопределённое поведение в самом PDFium, а не соглашение PDFiumPas, которое можно игнорировать по желанию: это может вернуть последние известные данные, вернуть ничего или уронить процесс, и какой из этих исходов случится в конкретной сборке — не то, на что коду приложения следует полагаться. Безопасное правило узкое: читайте Pdf.TextPage заново, непосредственно перед вызовом FPDFText_*, которому он нужен, и никогда не держите копию через оператор, который может отредактировать страницу
Пакетируйте правки, затем запрашивайте один раз
Ничто из этого не означает, что каждый вызов AddText или RemoveObject нуждается в защитном текстовом запросе сразу после себя для проверки результата. Каждый метод редактирования уже платит стоимость закрытия текстовой страницы один раз; запрос после каждой отдельной правки внутри цикла платит эту стоимость снова без всякой пользы, поскольку FPDFText_LoadPage заново обходит весь поток содержимого при каждом запуске
var
Pdf: TPdf;
I: Integer;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'watermarked.pdf';
Pdf.Active := True;
Pdf.PageNumber := 1;
// Strip every text object that looks like a draft watermark. Each
// RemoveObject call already invalidates the cache on its own, so
// nothing needs refreshing by hand between iterations
for I := Pdf.ObjectCount - 1 downto 0 do
if (Pdf.ObjectType[I] = otText) and (Pdf.ObjectBounds[I].Top > 700) then
Pdf.RemoveObject(I, True);
// Query once, after the whole batch is done, not once per removal
if Pdf.FindFirst('DRAFT') < 0 then
ShowMessage('Watermark cleared');
finally
Pdf.Free;
end;
end;
Та же логика пакетирования применяется конкретно к состоянию поиска. FindNext и FindPrevious продолжают сессию, начатую FindFirst, и эта сессия разрушается вместе со всем остальным, когда выполняется UnloadTextPage, так что повторный вызов FindNext после правки — вместо повторного вызова FindFirst — выбрасывает исключение, а не молча возобновляет поиск по содержимому, которого больше не существует. Относитесь к любой правке как к жёсткой границе и для текстового содержимого, и для позиции поиска, и позвольте одному свежему FindFirst по ту сторону ваших правок подхватить поиск заново
Как это соотносится с извлечением и работой с аннотациями
Обычное извлечение текста — чтение текста страницы без каких-либо изменений — никогда не сталкивается ни с чем из этого, потому что ничто не делает недействительным дескриптор, которого не коснулась ни одна правка. О том, как работают Text, прямоугольники символов и границы слов на немодифицированной странице, рассказывает сопутствующая статья об извлечении текста с PDFiumPas, покрывающая эту почву без жизненного цикла кэша текстовой страницы, который добавляет эта статья
Жизненный цикл кэша важнее всего в рабочих процессах, которые редактируют и сразу же действуют на основе результата: проставление исправления и его поиск, закрашивание абзаца и подтверждение, что он исчез, или поиск фразы, чтобы закрепить на ней аннотацию разметки сразу после вставки текста рядом с ней. Последний случай стоит отметить отдельно — аннотации разметки на основе quad-точек позиционируются по прямоугольникам символов, считанным с текстовой страницы, так что аннотация, построенная из координат, захваченных до правки, в итоге выделяет не то место, как только правка вступает в силу
API редактирования и работы с текстом в TPdf — часть компонента PDFium для Delphi и C++Builder, и на странице продукта представлен полный справочник методов для описанных здесь поверхностей редактирования, извлечения и поиска