PDFium Component dla Delphi wypełnia AcroForm atomowo przez TPdf.SetFormFieldValues: podajesz od 1 do 256 typowanych żądań adresowanych stroną i widgetem, a komponent albo aplikuje wszystkie wartości w jednej podmianie dokumentu, albo nie aplikuje żadnej. Gdy jedno żądanie jest nieważne, TPdfFormValuesWriteReport.FailedRequest nazywa żądanie, które zatopiło wsad
Ta gwarancja znaczy najwięcej w zadaniach, których nikt nie pilnuje. Nocny import pcha wiersze CRM do formularza roszczenia, albo usługa druku nakłada dane polisy na przeddrukowany szablon. Prowadź je pole po polu, a pierwsza zła wartość — kod, który nie jest ważnym eksportem radia, string dłuższy niż MaxLen pola — zostawi cię z PDF-em, który jest w połowie starymi danymi i w połowie nowymi. Nic nie crashnęło, więc nic nikogo nie zaalarmowało. API wsadowe istnieje po to, żeby porażka była czystą odmową zamiast pół zapisanego dokumentu
Dlaczego wypełnianie pole po polu jest niebezpieczne dla importu danych?
Wypełnianie pole po polu jest niebezpieczne, bo każdy zapis commituje się sam, więc błąd w środku pętli zostawia dokument pasujący ani do rekordu źródłowego, ani do oryginalnego szablonu. Klasyczny wzorzec to pętla po przypisaniach FormField[Index], każde idące przez natywne środowisko wypełniania formularzy PDFium widget po widgetcie. Wartości od 1 do 17 lądują, wartość 18 rzuca, a wywołujący musi teraz wiedzieć dokładnie, które wartości weszły, żeby je cofnąć
Większość wywołujących nigdy nie pisze tego kodu cofania, a ci, którzy piszą, zwykle robią to subtelnie źle: pole współdzielone zmieniło się już na każdej stronie, na której występuje, a stany checkboxów już się przekręciły. Jeśli zapis działa w bloku finally, pół wypełniony plik leci na dysk wyglądając kompletnie. W zadaniu nadruku to gorsze niż brak pliku, bo się drukuje i ktoś go podpisuje
Drugim cichym problemem jest fokus. Klasyczny setter prowadzi interaktywny edytor PDFium: zabija istniejący fokus, focusuje docelowy widget, zaznacza cały tekst, podmienia go i znów zabija fokus. Dla zadania headless to w porządku, ale w przeglądarce kończy też każdą sesję edycji, którą użytkownik miał otwartą w innym polu. Typowana ścieżka zapisu, która przyszła w PDFium Component 3.231.0, w ogóle nie przechodzi przez natywny fokus pól, a 3.233.0 rozszerzyła ją z pojedynczych pól na wsady międzystronicowe
Jak SetFormFieldValues aplikuje wsad w jednej podmianie
TPdf.SetFormFieldValues etapuje każde żądanie względem jednego uchwyconego snapshotu dokumentu, zapisuje wszystkie słowniki pól w jednym przebiegu i podmienia skończony wynik jedną podmianą. Oryginalny dokument pozostaje żywy aż do ostatniego kroku, więc każda porażka przed tym krokiem zostawia go nietkniętego
Sekwencja wyjaśnia i gwarancję, i koszt:
- Kontrole preflight idą pierwsze: dokument musi być aktywny, liczba żądań musi mieścić się w 1..256, żadna transakcja page-object ani edit-journal nie może być otwarta, żadne natywne pole nie może trzymać fokusu, a dokument musi dawać uprawnienie fill-forms albo modify
- Każde żądanie jest walidowane pod kątem zakresu stron i budżetu wejścia, a
FailedRequestśledzi właśnie sprawdzany indeks - Komponent uchwyca snapshot edycji, normalizuje AcroForm w tej kopii i aplikuje każde żądanie po kolei, pisząc
/V,/Idla wyborów choice i/ASdla każdego widgeta dzielącego pole - Zmiany są dokładane do snapshotu jako jedna rzadka aktualizacja inkrementalna w rozumieniu ISO 32000-1 §7.5.6, więc niepowiązane obiekty przechodzą bajt w bajt
- Osobna instancja weryfikatora ładuje najpierw kandydackie bajty; dopiero gdy otwiera się czysto, komponent wyładowuje żywy dokument i ładuje nowy, spadając na oryginalny snapshot, gdy to ładowanie pada
Wewnętrzny krok podmiany to ta sama machineria, której komponent używa dla snapshotów edit-journal, dlatego dziennik undo i odzyskiwania po crashu i zapis wsadowy nie mogą na siebie nachodzić: najpierw dokończ albo anuluj transakcję dziennika. Automatyczne akcje cyklu życia są tłumione, podczas gdy dokument jest podmieniany, więc skrypt otwierający dokument nie może przepisać pól ani złapać fokusu w środku podmiany
Budowanie typowanego wsadu żądań w Delphi
Wsad to dynamiczna tablica rekordów TPdfFormValueRequest, każdy z jednobazowym Page, zerobazowym lokalnym dla strony FieldIndex i typowanym TPdfFormValue. Rekord wartości niesie Kind równy fvkText, fvkChoice, fvkCheckBox albo fvkRadioButton, plus składowe Text, ChoiceIndices i Checked używane według tego rodzaju
Dane importu zwykle przychodzą kluczowane nazwami pól, więc pierwsza robota to tłumaczenie nazw na adresy strony i widgeta. FieldIndex liczy adnotacje widgetów na stronie w tej samej kolejności co FormFieldCount i FormFieldInfo, więc proste skanowanie działa. Helper bierze pierwszy widget znaleziony dla nazwy, co ma znaczenie dla pól współdzielonych, jak wyjaśnia następna sekcja
type
TFieldAddress = record
Page, FieldIndex: Integer;
end;
function FindFieldAddress(Pdf: TPdf; const FieldName: WString;
out Address: TFieldAddress): Boolean;
var
P, I: Integer;
begin
Result := False;
for P := 1 to Pdf.PageCount do
begin
Pdf.PageNumber := P;
for I := 0 to Pdf.FormFieldCount - 1 do
if Pdf.FormFieldInfo[I].Name = FieldName then
begin
Address.Page := P;
Address.FieldIndex := I;
Exit(True); // tylko pierwszy widget: jedno żądanie na pole
end;
end;
end;
procedure AddRequest(var Requests: TPdfFormValueRequests;
const FieldName: WString; Pdf: TPdf; const Value: TPdfFormValue);
var
Address: TFieldAddress;
N: Integer;
begin
if not FindFieldAddress(Pdf, FieldName, Address) then
raise Exception.CreateFmt('Template has no field "%s"', [FieldName]);
N := Length(Requests);
SetLength(Requests, N + 1);
Requests[N].Page := Address.Page;
Requests[N].FieldIndex := Address.FieldIndex;
Requests[N].Value := Value;
end;
Zauważ, że AddRequest rzuca przy nieznanej nazwie zamiast ją pominąć. Wsad waliduje adresy, nie twoje mapowanie, więc brakujące pole szablonu musi paść tutaj albo nigdy nie paść w ogóle. Z adresami rozwiązanymi, zbudowanie wartości i ich scommitowanie jest krótkie
var
Pdf: TPdf;
Requests: TPdfFormValueRequests;
Report: TPdfFormValuesWriteReport;
V: TPdfFormValue;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'claim-template.pdf';
Pdf.Active := True;
V := Default(TPdfFormValue);
V.Kind := fvkText;
V.Text := 'Maria Lopez';
AddRequest(Requests, 'Claimant.Name', Pdf, V);
V := Default(TPdfFormValue);
V.Kind := fvkCheckBox;
V.Checked := True;
AddRequest(Requests, 'Consent', Pdf, V);
V := Default(TPdfFormValue);
V.Kind := fvkRadioButton;
V.Text := 'Express'; // wartość eksportu wybieranego przycisku
AddRequest(Requests, 'Shipping', Pdf, V);
V := Default(TPdfFormValue);
V.Kind := fvkChoice;
SetLength(V.ChoiceIndices, 1);
V.ChoiceIndices[0] := 2; // indeks od 0 w tablicy Opt pola
AddRequest(Requests, 'Region', Pdf, V);
if Pdf.SetFormFieldValues(Requests, Report) then
Pdf.SaveAs('claim-filled.pdf')
else
LogBatchFailure(Requests, Report);
finally
Pdf.Free;
end;
end;
Typowane rodzaje są sprawdzane względem typu pola, zanim cokolwiek zostanie zapisane. Wartość tekstowa celująca w checkbox, wartość radia celująca w zwykły checkbox albo zapis do przycisku push — wszystkie są odrzucane. Indeksy choice muszą być ważne, unikalne i rosnące; więcej niż jeden indeks wymaga flagi MultiSelect, a wolny tekst w polu choice jest przyjmowany wyłącznie przez edytowalny combo box. Wartość tekstowa dłuższa niż MaxLen pola jest odrzucana zamiast być obcinana, a pola read-only odrzucają wprost. Dla grup radiowych z flagą NoToggleToOff pusta wartość eksportu — znacząca „wyczyść grupę" — też jest odrzucana
Dlaczego zapis wspólnego pola dwa razy zawala cały wsad?
TPdf.SetFormFieldValues odrzuca dwa żądania rozwiązujące się do tego samego pola terminalnego, bo oba chciałyby zapisać jedną wartość, a komponent odmawia pozwolenia jednemu żądaniu po cichu nadpisać drugie. Kontrola działa na właścicielu pola, nie na widgetcie, więc dwa różne widgety jednego pola na dwóch różnych stronach też liczą się jako duplikat
Powodem jest model pola z ISO 32000-1 §12.7.3.1. Pole terminalne posiada wartość; jego adnotacje widgetów to tylko miejsca, w których ta wartość jest wyświetlana. Pole o nazwie PolicyNumber pojawiające się w nagłówku każdej strony to jedno pole z kilkoma widgetami i każdy z tych widgetów pokazuje to samo /V. Grupa radiowa działa tak samo: każdy przycisk to widget, grupa to pole. Zapis PolicyNumber przez widget strony 1 i ponownie przez widget strony 9 to jedno pole zapisane dwa razy, a każda reguła wybierająca zwycięzcę chowa buga w twoim mapowaniu
Wsad traktuje to więc jako błąd i nazywa drugie żądanie w FailedRequest. W praktyce naprawa jest upstream: rozwiązuj każdą nazwę pola do dokładnie jednego adresu, jak robi FindFieldAddress, i pozwól komponentowi roznieść wartość do pozostałych widgetów. Robi to w ramach zapisu, ustawiając /AS na każdym widgetcie współdzielonego checkboxa albo grupy radiowej, żeby stan wyglądu zgadzał się z wartością pola — ten sam podział opisuje artykuł o wartości pola checkbox kontra stany widgetów
Czytanie raportu porażki
Nieudany wsad zwraca False, nie rzuca i zostawia oryginalny dokument i wszystkie wartości pól dokładnie takie, jakie były. TPdfFormValuesWriteReport raportuje potem Succeeded = False, zero w UpdatedFields i UpdatedWidgets, zerobazowy indeks winnego żądania w FailedRequest i czytelną dla człowieka przyczynę w ErrorMessage
procedure LogBatchFailure(const Requests: TPdfFormValueRequests;
const Report: TPdfFormValuesWriteReport);
var
R: Integer;
begin
R := Report.FailedRequest;
if R >= 0 then
Log(Format('Request %d (page %d, widget %d) rejected: %s',
[R, Requests[R].Page, Requests[R].FieldIndex, Report.ErrorMessage]))
else
Log('Batch rejected as a whole: ' + Report.ErrorMessage);
end;
FailedRequest równe -1 znaczy, że odrzucenie należy do transakcji, nie do żądania: natywny fokus był aktywny, dokument jest podpisany, formularz to XFA albo kontrola uprawnień padła. Wartość nieujemna wskazuje jeden wpis. Przy przepełnieniu budżetu zagregowanego wskazuje żądanie, które przepchnęło wsad ponad limit, co niekoniecznie jest żądaniem błędnym. Trzymaj równoległą tablicę nazw kolumn źródłowych obok Requests i zamienisz indeks 14 wprost na „wiersz 2087, kolumna ShipDate" w logu importu
Przy sukcesie UpdatedFields równa się liczbie żądań, a UpdatedWidgets liczy każdy dotknięty widget, współdzielone włącznie — tani test zdroworozsądkowy względem szablonu
Warunki wstępne i limity, na które trafisz
Najczęstsze odrzucenie w aplikacji przeglądarkowej to natywny fokus: gdy użytkownik pisze w polu, SetFormFieldValues odmawia, zanim w ogóle uchwyci snapshot, a oczekujący bufor edytora przeżywa nietknięty. Komponent nie zrzuci fokusu za twoimi plecami, bo zrobiłoby to commit albo stracił tekst, którego użytkownik nie dokończył. Wołaj ClearFormFieldFocus świadomie — kończy sesję edytora tak, jak kliknięcie poza polem — i dopiero potem ponów
Budżety są stałe i udokumentowane, więc możesz pod nie wymiarować importy:
- Od 1 do 256 żądań na wsad; większe rekordy dziel na kilka wsadów i zaakceptuj, że atomowość trzyma wtedy per wsad
- Najwyżej 262 144 jednostek UTF-16 tekstu i 4 096 indeksów choice na żądanie
- Najwyżej 1 048 576 jednostek UTF-16 tekstu i 16 384 indeksów choice na cały wsad
- Zwyczajne budżety dokumentu dla parsera snapshotów nadal obowiązują, łącznie z źródłem 64 MiB i 65 536 obiektów
Udana podmiana unieważnia też natywne uchwyty, wyszukiwania i zaznaczenia zdobyte przed wywołaniem, bo dokument pod spodem jest nowy. Aktywacje stron i widoków oraz logiczne kursory pól są trzymane; cokolwiek innego zcache'owałeś, jak uchwyty adnotacji albo page-object, musi być pobrane ponownie
Czym to się różni od importu XFDF?
TPdf.ImportXFDF i TPdf.SetFormFieldValues obie wkładają dane w pola, ale import XFDF to pobłażliwy merge, a wsad to surowa transakcja. Import XFDF dopasowuje wartości do pól po nazwie, pomija nazwy, których nie znajdzie, ignoruje duplikaty i zwraca, ile wpisów zaaplikował. To dokładnie właściwe dla scalania komentarzy recenzenta, gdzie częściowy wynik nadal jest użyteczny
Wsad robi odwrotnie w każdym punkcie. Adresuje widgety wprost, odrzuca cele nieznane albo złego typu, traktuje duplikaty jako błędy i nie ma częściowego wyniku. Używaj XFDF, gdy dane to plik wymiany z innego narzędzia; używaj SetFormFieldValues, gdy twoje własne kodowanie posiada mapowanie i pół zaaplikowany rekord byłby defektem
Czego zapis wsadowy nie pokrywa
Wsad to ograniczony profil AcroForm i mówi „nie", zamiast zgadywać poza tym profilem. Formularze XFA są odrzucane i potrzebują swojego dedykowanego edytora. Dokumenty podpisane są odrzucane, bo przepisywanie słowników pól pod istniejącym podpisem to workflow podpisu, nie operacja wypełniania; zaszyfrowane snapshoty też są poza profilem. Zepsute lub dwuznaczne tablice eksportu przycisków są odrzucane, a wartości eksportu radia pasujące do więcej niż jednego przycisku są odmawiane, chyba że grupa ustawia RadiosInUnison. Transakcja normalizuje też bezpośrednie słowniki pól przed zapisem — przypadek stojący za wartościami pól, które wyglądają, jakby wracały
Wyglądy zasługują na jedno zdanie troski. Dla zmienionych widgetów tekstowych i choice wsad wyrzuca nieaktualne /AP zamiast wysyłać wygląd pokazujący starą wartość i wyłącza NeedAppearances, żeby PDFium nie przebudowywał każdego niepowiązanego widgetu formularza. PDFium regeneruje brakujące wyglądy przy renderowaniu. Jeśli wypełniony PDF idzie do surowego pipeline druku, który nigdy nie syntezuje wyglądów, zrenderuj albo spłaszcz go najpierw przez PDFium i przetestuj tę ścieżkę na własnych szablonach
SetFormFieldValues i typowane rekordy wartości płyną w PDFium Component dla Delphi i Lazarus, gdzie strona produktu wylicza pełne API wypełniania formularzy i AcroForm obok kontrolek renderowania, edycji i przeglądania