TPdf.SetFocusedFormFieldText в компоненте PDFium пишет в живой буфер редактирования поля формы, находящегося сейчас в фокусе, а для формы XFA этот буфер никогда не доходит до пакета datasets, что сериализуется на диск, — так что значение, введённое пользователем и подтверждённое вашим кодом как принятое, молча исчезает при следующем открытии файла. У полей AcroForm этой проблемы нет: тот же самый вызов фиксируется в записи /V поля в момент, когда фокус перемещается прочь. Пользователь, заполняющий регистрационную форму XFA, сохраняющий её и открывающий заново, чтобы обнаружить поле суммы вновь пустым, сталкивается не с глюком отрисовки — он упирается в границу того, что сам движок PDFium предоставляет для записи данных формы
Это более узкий вопрос, чем обнаружение формы XFA как таковой или запуск её JavaScript: не «поддерживает ли PDFium XFA» и не «как мне выполнить скрипты AcroForm», а конкретно то, что происходит со значением после того, как SetFocusedFormFieldText сообщает об успехе. Короткая версия — AcroForm и XFA не два диалекта одной модели формы, если говорить о пути записи PDFium, — это две модели формы с двумя совершенно разными отношениями между тем, что печатает пользователь, и тем, что реально захватывает сохранение, и смешение этих двух — то, что превращает однострочный вызов API в тикет поддержки спустя три недели после запуска пилотного развёртывания у клиента. Статья о JavaScript в AcroForm показывает этот однострочный вызов и формулирует исход AcroForm-против-XFA в комментарии кода; эта статья остаётся на том же API и проходит через внутренний путь записи, доказательство через пакет datasets, что запись XFA никогда не доходит, почему пробел сидит в самом PDFium, а не в привязке Delphi, и обходное решение с патчингом собственного XML для документов, которым нужно, чтобы правка пережила сохранение
Как SetFocusedFormFieldText записывает значение поля?
TPdf.SetFocusedFormFieldText работает, симулируя правку на уровне нажатий клавиш, а не втыкая значение прямо в модель документа. Внутри он вызывает FORM_SelectAllText, чтобы выделить текущее содержимое сфокусированного поля, затем FORM_ReplaceSelection, чтобы перезаписать выделение новой строкой, — те же две операции, что запустило бы управляемое клавиатурой выделить-всё-и-напечатать. Поскольку запись идёт через путь интерактивного редактирования текста PDFium, а не в обход него, любой скрипт нажатия клавиши, форматирования или вычисления, привязанный к полю, срабатывает именно так, как сработал бы для человека, набирающего текст, что и делает этот API полезным для программного заполнения формы в просмотрщике, держащем JavaScript живым. Сторона чтения — FocusedFormFieldText, опирающаяся на FORM_GetFocusedText, и она отражает тот же живой буфер, что только что записал SetFocusedFormFieldText
if Pdf.FocusedFormFieldIndex >= 0 then
begin
if Pdf.SetFocusedFormFieldText('1284.50') then
Log('Buffer now reads: ' + Pdf.FocusedFormFieldText)
else
Log('No field is focused, or it does not accept text');
end
else
Log('Focus a field first - FocusFormField or a real click');
Почему AcroForm сохраняет значение, а XFA его теряет?
Текстовые и комбо-поля AcroForm сохраняются, потому что собственная среда заполнения форм PDFium сама фиксирует буфер редактирования: в момент, когда поле теряет фокус, буфер записывается в запись /V поля — тот же ключ, на который смотрит любая соответствующая спецификации программа чтения PDF, чтобы узнать сохранённое значение поля. TPdf.ClearFormFieldFocus — вызывающий FORM_ForceToKillFocus под капотом — принудительно вызывает эту фиксацию по требованию, так что коду, устанавливающему значение программно, не нужно ждать реального клика мышью где-то ещё в UI. Сохраните сразу после — и новый текст будет частью графа объектов документа ещё до того, как вообще выполнится TPdf.SaveAs, потому что /V — настоящая запись в настоящем словаре поля, а не что-то прикрученное постфактум
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
Pdf.ClearFormFieldFocus; // forces the /V commit now
Pdf.SaveAs('invoice-acroform.pdf');
// Reopen and confirm - this is an AcroForm document, so it holds
Pdf.Active := False;
Pdf.FileName := 'invoice-acroform.pdf';
Pdf.Active := True;
Pdf.FocusFormField(FieldIndex);
Assert(Pdf.FocusedFormFieldValue = '1284.50'); // passes
Где на самом деле живёт правка поля XFA?
У полей XFA нет такой проводки. Текст, введённый пользователем, попадает в буфер CPWL_Edit, принадлежащий слою рендеринга и взаимодействия XFA в PDFium, и у этого слоя нет пути кода, копирующего буфер обратно в пакет datasets, хранимый в PDF. TPdf.GetXfaDatasets делает этот пробел видимым: вызовите его до и после правки поля XFA, и возвращённые байты будут идентичны, потому что метод читает исходный пакет, с которым был открыт документ, а никогда живое состояние виджета, который вы только что отредактировали. Ничто из этого не ошибка кэширования или проблема таймингов обновления — пакет datasets на диске и буфер редактирования в памяти попросту два разных фрагмента состояния, которые публичный API PDFium никогда не связывает
var
Before, After: TBytes;
begin
Before := Pdf.GetXfaDatasets;
Pdf.FocusFormField(FieldIndex);
Pdf.SetFocusedFormFieldText('1284.50');
After := Pdf.GetXfaDatasets;
// Before and After are byte-for-byte identical on an XFA document -
// the edit never touched the packet GetXfaDatasets reads from
end;
Это ошибка компонента PDFium или ограничение самого PDFium?
Недостающая часть находится в самом PDFium, а не в привязке Delphi поверх него. У публичного API PDFium нет FPDF_SetXFAPacket для инъекции обновлённого пакета и нет FPDF_SaveAsXFA, чтобы попросить движок XFA сериализовать его текущий DOM обратно в XML datasets перед сохранением. FPDF_SaveAsCopy — экспорт, на который опирается TPdf.SaveAs, — записывает граф объектов документа, что уже есть у PDFium; у него нет крючка, чтобы попросить движок XFA сначала сбросить своё живое состояние, потому что этого крючка не существует выше по потоку. Компонент PDFium не может добавить согласование, которое сам PDFium никогда не реализовывал, а поставка самодельного сериализатора DOM-в-XML, угадывающего внутреннее состояние XFA в PDFium, была бы хуже, чем честный пробел: он выглядел бы рабочим до тех пор, пока следующая версия PDFium не изменит что-то, чего никто вне проекта не видит
Эта граница всплыла во время того же аудита v2.13.2, что изначально построил SetFocusedFormFieldText. FORM_ReplaceSelection был привязан в таблице импорта DLL на протяжении нескольких версий, ни разу не будучи вызванным из кода на Pascal, и добавление пути записи, что наконец его использовал, — именно то, что сделало пробел устойчивости достаточно конкретным для документирования, а не теоретическим. Тот же раунд аудита выявил не связанный, но родственный по духу пробел: JavaScript AcroForm был молча отключён начиная с v2.13.0, потому что платформа JS подключалась только внутри ветки инициализации XFA, так что обычные документы AcroForm с app.alert или вычисляемыми полями вообще никогда не получали движок скриптов. Тот пробел оказался исправимым — расширением платформы JS на каждый документ независимо от XFA, — и вышел в том же релизе; описанный здесь пробел устойчивости исправимым не был, по причинам выше. Исправление JavaScript и события отклонения хоста вокруг него описаны в статье о запуске JavaScript AcroForm с компонентом PDFium
Что с этим делать в Delphi?
Для документов AcroForm исправление — не более чем хорошая привычка: вызывайте ClearFormFieldFocus (или иначе перемещайте фокус прочь) перед SaveAs всякий раз, когда значение было установлено программно, вместо того чтобы предполагать, что более позднее взаимодействие с UI запустит фиксацию за вас. Для документа, который может быть либо AcroForm, либо XFA, — а это обычный случай в просмотрщике общего назначения, — проверяйте FormType или булево XFA прежде, чем обещать вызывающему коду, что сохранение удержится, и прочтите статью об обнаружении форм XFA и извлечении пакетов XFA для полного набора проверок, включая случай XFAF, где содержимое XFA наслоено поверх во всём остальном обычных виджетов AcroForm, которые действительно соблюдают /V
Для настоящей динамической формы XFA, где отредактированные значения должны пережить сохранение, живой буфер редактирования — вообще не тот инструмент. Надёжный путь — относиться к GetXfaDatasets как к вашей отправной точке, а не результату: прочитайте его один раз при открытии документа, ведите собственную запись того, что пользователь изменил, поле за полем, — именно те значения, что уже есть в вашем UI, поскольку PDFium не вернёт их вам постфактум, — сами подправьте их в базовый XML и управляйте собственным выводом. Запись, идущая через XML, который контролирует ваш собственный код, переживает сохранение так, как никогда не смог бы буфер CPWL_Edit
function ExportEditedXfaValue(Pdf: TPdf; const FieldPath,
NewValue: string): TBytes;
var
DatasetsXml: string;
begin
// GetXfaDatasets ships with PDFium Component; PatchXmlNode below is
// your own helper over your own XML library, nothing PDFium provides
DatasetsXml := TEncoding.UTF8.GetString(Pdf.GetXfaDatasets);
DatasetsXml := PatchXmlNode(DatasetsXml, FieldPath, NewValue);
Result := TEncoding.UTF8.GetBytes(DatasetsXml);
end;
Обнаружение пробела прежде, чем это сделает клиент
TPdf.SaveAs возвращает True независимо от того, пережило значение поля XFA сохранение или нет, потому что с точки зрения PDFium сохранение реально удалось — оно записало каждый байт, который его попросили записать. Именно это делает данный дефект тем видом, что проскальзывает мимо дымового теста и доходит до клиента: ничто не выбрасывает исключение, ничто не логируется, файл прекрасно открывается, неверно только конкретное значение. Тест полного цикла, реально переоткрывающий сохранённый файл и сравнивающий значение поля — или сравнивающий GetXfaDatasets до и после, по образцу примера выше, — должен входить в набор регрессионных тестов для любого просмотрщика, позволяющего пользователям редактировать содержимое XFA, не только пути AcroForm, что случайно работают по умолчанию
Ничто из этого не дефект, который стоит заводить против компонента PDFium, — скорее граница, вокруг которой стоит проектировать: SetFocusedFormFieldText делает в точности то, что говорит его название, для обеих моделей форм, а различие в результате чисто прослеживается до того, к чему AcroForm и XFA каждый подключают этот буфер на стороне PDFium. API, примитивы фокуса и сохранения, а также читатели пакетов, упомянутые здесь, — часть компонента PDFium для Delphi и C++Builder