TPdf.SetFocusedFormFieldText w komponencie PDFium zapisuje do żywego bufora edycji aktualnie zaznaczonego pola formularza, a dla formularza XFA ten bufor nigdy nie dociera do pakietu datasets, który zostaje zserializowany na dysk — więc wartość, którą użytkownik wpisał, i którą twój kod potwierdził jako przyjętą, po cichu znika przy następnym otwarciu pliku. Pola AcroForm nie mają tego problemu: to samo wywołanie zatwierdza się do wpisu /V pola w chwili, gdy fokus się przesuwa. Użytkownik, który wypełnia formularz wniosku XFA, zapisuje, a potem otwiera go ponownie i widzi puste pole kwoty, nie natrafia na usterkę renderowania — natrafia na granicę tego, co sam silnik PDFium udostępnia do zapisu danych formularza
To węższe pytanie niż wykrycie formularza XFA w ogóle, czy uruchomienie jego JavaScriptu: nie "czy PDFium obsługuje XFA" i nie "jak wykonać skrypty AcroForm", tylko konkretnie co dzieje się z wartością po tym, jak SetFocusedFormFieldText zgłosi sukces. Krótka wersja jest taka, że AcroForm i XFA nie są dwoma dialektami tego samego modelu formularza, jeśli chodzi o ścieżkę zapisu PDFium — to dwa modele formularzy z dwoma zupełnie różnymi relacjami między tym, co wpisuje użytkownik, a tym, co faktycznie przechwytuje zapis, a pomieszanie tych dwóch zamienia jednolinijkowe wywołanie API w zgłoszenie do wsparcia trzy tygodnie po uruchomieniu pilotażowego wdrożenia u klienta. Artykuł o JavaScript AcroForm pokazuje to jednolinijkowe wywołanie i stwierdza różnicę wyniku AcroForm-kontra-XFA w komentarzu kodu; ten artykuł pozostaje przy tym samym API i przechodzi przez wewnętrzną ścieżkę zapisu, dowód na pakiecie datasets, że zapis XFA nigdy nie ląduje, dlaczego ta luka siedzi w samym PDFium, a nie w wiązaniu Delphi, oraz obejście polegające na łataniu własnego XML dla dokumentów, które potrzebują, aby edycja przetrwała zapis
Jak SetFocusedFormFieldText zapisuje wartość pola?
TPdf.SetFocusedFormFieldText działa, symulując edycję na poziomie naciśnięć klawiszy, a nie wtykając wartość bezpośrednio do modelu dokumentu. Wewnętrznie wywołuje FORM_SelectAllText, aby zaznaczyć bieżącą zawartość zaznaczonego pola, a następnie FORM_ReplaceSelection, aby nadpisać zaznaczenie nowym łańcuchem znaków — te same dwie operacje, jakie wyzwoliłoby zaznaczenie-wszystkiego-i-wpisanie sterowane klawiaturą. Ponieważ zapis przechodzi przez interaktywną ścieżkę edycji tekstu PDFium, a nie ją omija, każdy skrypt naciśnięcia klawisza, formatowania czy obliczenia powiązany z polem uruchamia się dokładnie tak, jak uruchomiłby się dla człowieka piszącego, co czyni to API użytecznym do programowego wypełniania formularzy w przeglądarce utrzymującej JavaScript przy życiu. Odpowiednikiem po stronie odczytu jest FocusedFormFieldText, wspierane przez FORM_GetFocusedText, i odzwierciedla ono ten sam żywy bufor, który właśnie zapisał 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');
Dlaczego AcroForm zachowuje wartość, a XFA ją traci?
Pola tekstowe i combo AcroForm przetrwają, ponieważ własne środowisko wypełniania formularzy PDFium zatwierdza za ciebie bufor edycji: w chwili, gdy pole traci fokus, bufor jest zapisywany do wpisu /V pola, tego samego klucza, na który patrzy każdy zgodny czytnik PDF, aby poznać zapisaną wartość pola. TPdf.ClearFormFieldFocus — który wywołuje pod maską FORM_ForceToKillFocus — wymusza to zatwierdzenie na żądanie, więc kod, który ustawia wartość programowo, nie musi czekać na prawdziwe kliknięcie myszą gdzie indziej w interfejsie. Zapisz zaraz potem, a nowy tekst jest częścią grafu obiektów dokumentu, zanim TPdf.SaveAs w ogóle się uruchomi, ponieważ /V to prawdziwy wpis w prawdziwym słowniku pola, nie coś doklejonego później
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
Gdzie faktycznie mieszka edycja pola XFA?
Pola XFA nie mają takiego okablowania. Tekst, który wpisuje użytkownik, ląduje w buforze CPWL_Edit, który należy do warstwy renderowania i interakcji XFA w PDFium, a ta warstwa nie ma żadnej ścieżki kodu, która kopiowałaby bufor z powrotem do pakietu datasets przechowywanego w PDF. TPdf.GetXfaDatasets czyni tę lukę widoczną: wywołaj ją przed i po edycji pola XFA, a bajty, które zwraca, są identyczne, ponieważ metoda czyta oryginalny pakiet, z jakim dokument został otwarty, nigdy żywy stan widgetu, który właśnie edytowałeś. Nic w tym nie jest błędem cache'owania ani problemem z czasem odświeżania — pakiet datasets na dysku i bufor edycji w pamięci to po prostu dwa różne fragmenty stanu, których publiczne API PDFium nigdy nie łączy
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;
Czy to błąd komponentu PDFium, czy ograniczenie samego PDFium?
Brakujący element siedzi w samym PDFium, nie w wiązaniu Delphi na nim zbudowanym. Publiczne API PDFium nie ma FPDF_SetXFAPacket, aby wstrzyknąć zaktualizowany pakiet, ani FPDF_SaveAsXFA, aby poprosić silnik XFA o zserializowanie swojego bieżącego DOM z powrotem do XML datasets przed zapisem. FPDF_SaveAsCopy — eksport wspierający TPdf.SaveAs — zapisuje graf obiektów dokumentu, który PDFium już posiada; nie ma żadnego haka, aby poprosić silnik XFA o zrzucenie najpierw swojego żywego stanu, ponieważ ten hak nie istnieje wyżej w potoku. Komponent PDFium nie może dodać uzgadniania, którego samo PDFium nigdy nie zaimplementowało, a dostarczenie domowej roboty serializatora DOM-do-XML, który zgaduje wewnętrzny stan XFA PDFium, byłoby gorsze niż uczciwa luka: wyglądałoby, jakby działało, dopóki kolejna wersja PDFium nie zmieni czegoś, czego nikt spoza projektu nie widzi
Ta granica ujawniła się podczas tego samego audytu v2.13.2, który w ogóle zbudował SetFocusedFormFieldText. FORM_ReplaceSelection był związany w tabeli importu DLL przez wersje bez nigdy niewywoływania go z kodu Pascala, a dodanie ścieżki zapisu, która w końcu go użyła, jest tym, co uczyniło lukę utrwalania na tyle konkretną, by ją udokumentować, zamiast pozostawić teoretyczną. Ta sama runda audytu ujawniła niepowiązaną, ale pokrewną w duchu lukę: JavaScript AcroForm był po cichu wyłączony od v2.13.0, ponieważ platforma JS była okablowana wyłącznie wewnątrz gałęzi inicjalizacji XFA, więc zwykłe dokumenty AcroForm z app.alert czy polami obliczanymi w ogóle nie dostawały silnika skryptowego. Ten problem dał się naprawić — rozszerzając platformę JS na każdy dokument niezależnie od XFA — i został wydany w tej samej wersji; luka utrwalania opisana tutaj nie dała się naprawić, z powodów opisanych powyżej. Poprawka JavaScript i zdarzenia weta hosta wokół niej są omówione w uruchamianiu JavaScript AcroForm za pomocą komponentu PDFium
Co powinieneś z tym zrobić w Delphi?
Dla dokumentów AcroForm poprawka to nic więcej niż dobry nawyk: wywołaj ClearFormFieldFocus (lub inaczej przesuń fokus) przed SaveAs za każdym razem, gdy wartość została ustawiona programowo, zamiast zakładać, że późniejsza interakcja z interfejsem wyzwoli za ciebie zatwierdzenie. Dla dokumentu, który może być albo AcroForm, albo XFA — co jest typowym przypadkiem w przeglądarce ogólnego przeznaczenia — sprawdź FormType lub wartość logiczną XFA, zanim obiecasz wywołującemu, że zapis się utrwali, a przeczytaj wykrywanie formularzy XFA i wyodrębnianie pakietów XFA, aby poznać pełen zestaw sond, w tym przypadek XFAF, gdzie zawartość XFA jest nałożona na skądinąd zwykłe widgety AcroForm, które honorują /V
Dla prawdziwego dynamicznego formularza XFA, w którym edytowane wartości muszą przetrwać zapis, interaktywny bufor edycji w ogóle nie jest właściwym narzędziem. Trwała ścieżka to potraktowanie GetXfaDatasets jako twojej linii bazowej, nie twojego wyniku: odczytaj ją raz, gdy dokument się otwiera, prowadź własny rejestr tego, co użytkownik zmienił, pole po polu — dokładnie wartości, które twój interfejs już posiada, ponieważ PDFium nie odda ci ich z powrotem po fakcie — załataj je do bazowego XML samodzielnie, i steruj własnym wyjściem. Zapis, który przechodzi przez XML kontrolowany przez twój własny kod, przetrwa zapis, którego bufor CPWL_Edit nigdy by nie przetrwał
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;
Wychwytywanie tej luki, zanim zrobi to klient
TPdf.SaveAs zwraca True niezależnie od tego, czy wartość pola XFA przetrwała, ponieważ z punktu widzenia PDFium zapis naprawdę się powiódł — zapisał każdy bajt, o który go poproszono. To czyni to dokładnie tym rodzajem defektu, który prześlizguje się przez test dymny i dociera do klienta: nic nie zgłasza wyjątku, nic nie loguje, plik otwiera się poprawnie, tylko konkretna wartość jest błędna. Test round-trip, który faktycznie ponownie otwiera zapisany plik i porównuje wartość pola — lub porównuje GetXfaDatasets przed i po, zgodnie z wcześniejszym przykładem — należy do zestawu testów regresyjnych każdej przeglądarki, która pozwala użytkownikom edytować zawartość XFA, nie tylko ścieżek AcroForm, które akurat działają domyślnie
Nic z tego nie jest defektem do zgłoszenia przeciwko komponentowi PDFium, tyle co granicą, wokół której trzeba projektować: SetFocusedFormFieldText robi dokładnie to, co mówi jego nazwa dla obu modeli formularzy, a różnica w wyniku wywodzi się wprost z tego, do czego AcroForm i XFA każdy z osobna podłącza ten bufor po stronie PDFium. API, prymitywy fokusu i zapisu oraz czytniki pakietów, do których się tu odwołano, są częścią komponentu PDFium dla Delphi i C++Buildera