TPdf.SetFocusedFormFieldText в PDFium Component записва в активния буфер за редактиране на текущо фокусираното формулярно поле, а при XFA формуляр този буфер никога не достига до пакета datasets, който се сериализира на диска — затова стойност, въведена от потребителя и потвърдена от кода ви, изчезва при следващото отваряне на файла. При AcroForm полетата този проблем не съществува: същото извикване записва стойността в записа /V на полето веднага щом фокусът се премести. Ако потребител попълни XFA формуляр, запише го и го отвори отново, за да види празно поле за сума, това не е дефект при рендиране — това е границата на възможностите, които самият PDFium предоставя за запис на данни във формуляри
Това е по-тесен въпрос от първоначалното откриване на XFA формуляр или от стартирането на неговия JavaScript: не става дума за „поддържа ли PDFium XFA“ и не става дума за „как да изпълня скриптове на AcroForm“, а конкретно за това какво се случва със стойността, след като SetFocusedFormFieldText върне успех. Накратко, за пътя за запис на PDFium AcroForm и XFA не са два диалекта на един и същ модел на формуляр — те са два модела с напълно различна връзка между въведеното от потребителя и данните, които записът улавя, а смесването им превръща едноредово API извикване в заявка за поддръжка седмици след началото на пилотното внедряване при клиента. Статията за JavaScript на AcroForm показва едноредовото извикване и посочва резултата за AcroForm и XFA в коментар към кода; тази статия остава при същия API и проследява вътрешния път за запис, доказателството от пакета datasets, че записът в XFA не достига до него, причината границата да е в самия PDFium, а не в Delphi binding-а, и обходен вариант със собствена 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 Component ли е или ограничение на PDFium?
Липсващата част е в самия PDFium, а не в Delphi binding-а над него. Публичният API на PDFium няма FPDF_SetXFAPacket за въвеждане на обновен пакет и няма FPDF_SaveAsXFA, с което да поискате от XFA engine да сериализира текущия си DOM обратно в XML на datasets преди запис. FPDF_SaveAsCopy — експортът, върху който се основава TPdf.SaveAs — записва графа на обектите, с който PDFium вече разполага; той няма кука, с която първо да поиска от XFA engine да изчисти активното си състояние, защото такава кука не съществува в upstream кода. PDFium Component не може да добави синхронизация, която самият PDFium не е реализирал, а собствен сериализатор DOM-към-XML, който гадае за вътрешното състояние на XFA в PDFium, би бил по-лош от честно описаното ограничение: ще изглежда работещ, докато следващата версия на PDFium не промени нещо, което никой извън проекта не може да види
Тази граница се появи по време на същия одит на v2.13.2, при който беше изграден SetFocusedFormFieldText. FORM_ReplaceSelection беше свързан в таблицата за импортиране на DLL за версии, без някога да се извиква от Pascal код, а добавянето на пътя за запис, който най-накрая го използва, направи проблема с постоянството достатъчно конкретен, за да бъде описан, а не само теоретичен. Същият одит откри и друга, свързана по дух празнина: JavaScript на AcroForm беше мълчаливо изключен от v2.13.0, защото JS платформата беше свързана само в клона за инициализация на XFA, така че обикновените документи на AcroForm с app.alert или изчисляеми полета изобщо не получаваха скриптов engine. Този проблем можеше да се поправи — JS платформата беше разширена за всеки документ независимо от XFA и промяната излезе в същата версия; разглежданото тук ограничение при записа не можеше да се поправи по изложените причини. Поправката за JavaScript и събитията около отказа от страна на хоста са описани в изпълнение на JavaScript на AcroForm с PDFium Component
Какво трябва да направите в 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 Component, колкото граница, около която трябва да проектирате: SetFocusedFormFieldText прави точно това, което името му казва, и при двата модела на формуляр, а разликата в резултата произтича директно от това към какво AcroForm и XFA свързват този буфер от страната на PDFium. API, примитивите за фокус и запис и четците на пакети, посочени тук, са част от PDFium Component за Delphi и C++Builder