Ви викликаєте 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 points) позиціюються за прямокутниками символів, зчитаними з текстової сторінки, тож анотація, побудована з координат, захоплених до редагування, зрештою підсвічує неправильне місце, щойно редагування приземлиться
API редагування й тексту TPdf — частина компонента PDFium для Delphi та C++Builder, і сторінка продукту містить повний довідник методів для поверхонь редагування, вилучення та пошуку, розглянутих тут