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 під капотом — примусово змушує цю фіксацію на вимогу, тож коду, що встановлює значення програмно, не доводиться чекати на справжнє клацання миші деінде в інтерфейсі. Збережіть одразу після цього, і новий текст стає частиною графа об'єктів документа ще до того, як взагалі виконається 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 щоразу, коли значення було встановлено програмно, а не припускайте, що пізніша взаємодія з інтерфейсом спричинить фіксацію за вас. Для документа, що може бути або AcroForm, або XFA — звичайний випадок у загального призначення переглядачі — перевіряйте FormType чи булеве XFA, перш ніж обіцяти викликачу, що збереження закріпиться, і читайте виявлення форм XFA та вилучення пакетів XFA для повного набору перевірок, включно з випадком XFAF, де вміст XFA накладений на в іншому звичайні віджети AcroForm, що справді дотримуються /V
Для справжньої динамічної форми XFA, де відредаговані значення мають пережити збереження, інтерактивний буфер редагування взагалі не той інструмент. Надійний шлях — трактувати GetXfaDatasets як вашу базову лінію, а не результат: прочитати її один раз, коли документ відкривається, вести власний облік того, що користувач змінив, поле за полем — точнісінько ті значення, які вже має ваш інтерфейс, оскільки 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