HotPDF Delphi Component wypełnia istniejące pole AcroForm we wczytanym PDF przez THotPDF.SetFormFieldValue, adresowane albo liczonym od zera indeksem pola, albo pełną nazwą kwalifikowaną. Zapisanie nowego wpisu /V jest łatwą częścią; to, co czyni to wywołanie niezawodnym na prawdziwych formularzach, to fakt, że ta sama metoda utrzymuje spójność trzech kawałków stanu niewidocznych, dopóki się nie zepsują: zdekodowanej tożsamości pola, dzięki której nazwa spoza ASCII da się w ogóle znaleźć, stanu wyglądu /AS na widgetach checkboxów i przycisków radio oraz tablicy indeksów wyboru /I na polach wyboru. Widoczny strumień wyglądu to osobny, jawny krok przez EnsureLoadedFieldAppearanceStream
Scenariusz jest przyziemny: klient przysyła ci swój formularz, deklarację podatkową, zgłoszenie szkody, zamówienie zbudowane przez kogoś w Acrobacie lata temu, a twoja aplikacja Delphi ma go wypełnić z bazy danych i oddać plik, który otwiera się poprawnie wszędzie. Nie masz żadnej kontroli nad tym, jak formularz powstał. Nazwy pól mogą być zakodowane w UTF-16, wartości eksportowe checkboxów mogą wynosić 2 zamiast Yes, a pola kombi mogą używać par opcji [export display]. Każdy z tych szczegółów ma regułę w ISO 32000-1 i każdą z tych reguł SetFormFieldValue obsługuje teraz za ciebie. Ten artykuł jest o tym, co robi, dlaczego i gdzie się zatrzymuje. Pokrewny problem tworzenia pól, których jeszcze nie ma, omawia dodawanie pól AcroForm do wczytanego PDF w Delphi
Dlaczego SetFormFieldValue nie znajduje pola o nazwie spoza ASCII?
Przed wersją v2.752.1 odpowiedzią było kodowanie: pole żyło w pliku pod szesnastkową nazwą UTF-16BE, a cache nazw trzymał zapis heksowy zamiast tekstu. ISO 32000-1 §12.7.3.1 definiuje częściową nazwę pola /T jako łańcuch tekstowy, a §7.9.2.2 mówi, że łańcuch tekstowy może być UTF-16BE z wiodącym znacznikiem kolejności bajtów FE FF. Narzędzia do tworzenia formularzy rutynowo serializują takie nazwy jako łańcuchy heksowe zgodnie z §7.3.4.3, więc pole o nazwie Straße przychodzi jako <FEFF005300740072006100DF0065>. Wewnątrz HotPDF THPDFStringObject.Value trzyma surowy tekst szesnastkowy, gdy ustawione jest IsHexadecimal, co jest dokładnie tym, czego chcesz dla bezstratnej podróży w obie strony oryginalnego słownika, i dokładnie tym, czego nie chcesz jako klucza wyszukiwania. HPDFLoadedFormTextName rozdziela te dwie sprawy. Gdy budowany jest cache relacji, każda wartość /T przechodzi przez tę funkcję: jeśli obiekt łańcucha jest szesnastkowy, HPDFHexToBytes odtwarza sekwencję bajtów; jeśli bajty zaczynają się od FE FF i mają parzystą długość, ładunek jest dekodowany jako UTF-16BE i ponownie kodowany jako UTF-8; wynik jest potem sklejany z nazwą rodzica kropką, tworząc pełną nazwę kwalifikowaną opisaną w §12.7.3.1, więc dziecko o nazwie City pod rodzicem o nazwie Address jest rejestrowane jako Address.City. Klucz w cache jest normalizowany do małych liter, dzięki czemu SetFormFieldValue('address.city', ...) też się udaje; to wygoda ponad standard, bo specyfikacja traktuje nazwy jako rozróżniające wielkość liter. Najważniejsze, że zmienia się tylko klucz w cache. Obiekt /T w słowniku pola zachowuje swoje szesnastkowe kodowanie, więc zapis dokumentu nie przepisuje tożsamości pola, które tylko wypełniłeś
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Nazwy kwalifikowane są dekodowane z łańcuchów /T w UTF-16BE i
// sklejane kropkami, więc nazwy zagnieżdżone i spoza ASCII się rozwiązują
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// Wartości spoza Latin-1 podróżują jako szesnastkowy UTF-16BE z przedrostkiem FEFF
// i są zapisywane jako szesnastkowy łańcuch PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
Co SetFormFieldValue właściwie zapisuje?
Obie przeciążone wersje wykonują te same pięć kroków: zlokalizować słownik pola, zapisać /V przez HPDFSetDictFormValue, uzgodnić indeksy wyboru pola choice, oznaczyć słownik jako brudny, uzgodnić stany wyglądu przycisków i na koniec zapisać indeks pola przez NoteLoadedFormFieldDirty. Ten ostatni krok ma znaczenie, jeśli formularz niesie skrypty obliczeń, bo zbiór brudnych pól jest tym, co bezparametrowa wersja RecalculateLoadedFormFieldsIncremental konsumuje, żeby ponownie uruchomić tylko te obliczenia, które przechodnio czytają zmienione pole. Sam HPDFSetDictFormValue uważa na typ obiektu, który zastępuje. Jeśli istniejące /V jest obiektem nazwy, czego checkboxy i przyciski radio używają jako wartości eksportowej, nowa wartość jest zapisywana jako nazwa, nigdy jako łańcuch, bo nazwy PDF są z konstrukcji wyłącznie ASCII. W przeciwnym razie zapisuje obiekt łańcucha i bada wartość, którą przekazałeś: łańcuch zaczynający się od FEFF, o parzystej długości i złożony wyłącznie z cyfr szesnastkowych, jest traktowany jako postać przewodowa UTF-16BE z §7.9.2.2 i zapisywany z ustawionym IsHexadecimal, więc serializuje się jako <FEFF...>, a nie jako literalne (FEFF...). Na tym mechanizmie opiera się linia z City powyżej; każdy inny łańcuch jest zapisywany jako łańcuch literalny z bajtami, które podałeś, więc dla zwykłego tekstu łacińskiego przekazujesz zwykły tekst
Dlaczego checkbox zachowuje stary znacznik po zmianie wartości?
Bo dla pola przycisku sama wartość nie decyduje o tym, co jest rysowane. ISO 32000-1 §12.7.4.2.3 określa, że widget checkboxa nosi stan wyglądu /AS wskazujący, który strumień w /AP /N jest aktualnie pokazywany, a czytniki malują z /AS, nie z /V. Jeśli zmienisz /V na Yes, a zostawisz /AS na Off, plik jest wewnętrznie sprzeczny, a spłaszczanie ochoczo wpiecze na stronę nieaktualny, niezaznaczony wygląd, podczas gdy dane formularza mówią, że pole jest zaznaczone. ReconcileLoadedButtonAppearanceStates istnieje po to, żeby tę lukę zamknąć: dla pola, którego /FT to Btn, odwiedza sam słownik pola oraz każdy wpis w jego tablicy /Kids, czyta nazwę stanu włączenia z /AP /N i przepisuje /AS na tę nazwę, gdy pasuje do wartości pola, albo na Off, gdy nie pasuje
Dwa szczegóły z prawdziwych formularzy ukształtowały poprawkę w v2.752.3. Po pierwsze, słownik wyglądu normalnego może zawierać wyłącznie stan włączenia; §12.7.4.2.3 nazywa wygląd wyłączenia Off, ale narzędzia autorskie często pomijają jego strumień i pozwalają czytelnikowi nie rysować niczego. Wcześniejszy kod wycofywał się, gdy słownik trzymał mniej niż dwa wpisy, więc te jednostanowe checkboxy po cichu zachowywały stary znacznik. Sprawdzenie jest teraz po prostu takie, że słownik nie jest pusty, a nazwa stanu włączenia jest brana jako pierwszy klucz inny niż Off. Po drugie, nazwa stanu włączenia jest taka, jaką wybrał autor. Prawdziwe formularze używają 2, Yes, On albo słowa zlokalizowanego, więc porównanie idzie z rzeczywistym kluczem, bez rozróżniania wielkości liter, nigdy z zakodowanym na sztywno Yes. Przyciski radio dodają jeszcze jedną komplikację opisaną w §12.7.4.2.4: wybór mieszka w /V na polu nadrzędnym, a poszczególne dzieci są właścicielami widgetów i zwykle nie mają własnego /V. Zagnieżdżony helper InheritedButtonValue idzie więc w górę po łańcuchu /Parent, do 64 poziomów, aż znajdzie niepustą wartość, więc każde dziecko jest porównywane z wartością grupy, do której należy. Ustawienie rodzica na wartość eksportową jednego dziecka włącza dokładnie to dziecko i wyłącza każdego sąsiada
// Checkbox: wartość eksportowa musi pasować do klucza stanu włączenia w /AP /N
// (często 'Yes', ale prawdziwe formularze używają '2', 'On' albo czegokolwiek innego)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Grupa radio: /V jest zapisywane na rodzicu; każdy widget dziecka dostaje
// /AS ustawione na swoją nazwę eksportową albo na Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Wyczyszczenie checkboxa: każda wartość, która nie pasuje do żadnego stanu włączenia, daje /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Pola wyboru: trzymanie /I w kroku z /V
Dla pola kombi albo listy rozwijanej /V to nie jedyne miejsce, w którym zapisany jest wybór. Tabela 231 w §12.7.4.4 definiuje /I jako tablicę liczonych od zera indeksów w /Opt, które wskazują wybrane pozycje, a czytelnik, który znajdzie /I wskazujące opcję 0, gdy /V wymienia opcję 3, może podświetlić niewłaściwy wiersz. Od wersji v2.754.1 HPDFReconcileChoiceSelection biegnie wewnątrz każdego wywołania SetFormFieldValue i gdy odziedziczone /FT to Ch, odbudowuje /I z nowej wartości. Kolejność operacji jest celowa. Lokalny wpis /I jest najpierw usuwany, bez dotykania jego zawartości: gdyby stara tablica była obiektem pośrednim współdzielonym z innym polem, zmiana jej w miejscu uszkodziłaby wybór tamtego pola, więc procedura porzuca odwołanie i tworzy świeżą tablicę bezpośrednią. Potem rozwiązuje /Opt przez łańcuch /Parent, bo opcje wyboru mogą być dziedziczone, i skanuje wpisy. Goła opcja łańcuchowa jest porównywana wprost; para [export display] jest porównywana po swoim elemencie eksportowym, a para z mniej niż dwoma elementami jest pomijana. Obie strony przechodzą przez HPDFLoadedFormTextName, więc szesnastkowa opcja UTF-16 pasuje do szesnastkowej wartości UTF-16, nawet jeśli ich nie zapiszesz identycznie. Przy pierwszym dopasowaniu zapisywane jest jednoelementowe /I i skan się zatrzymuje; wartość skalarna zawsze zastępuje poprzedni wielokrotny wybór, niezależnie od flagi MultiSelect
Gdy nic nie pasuje, żadne /I nie jest zapisywane. To poprawne zachowanie dla edytowalnego pola kombi, w którym §12.7.4.4 pozwala użytkownikowi wpisać wartość spoza listy opcji; taka wartość nie ma indeksu, a nieaktualny indeks byłby gorszy niż żaden. To samo dostaniesz, gdy przekażesz etykietę wyświetlania zamiast wartości eksportowej do listy opcji z parami, więc gdy pole kombi odmawia pokazania twojego wyboru, sprawdź, którą połowę pary podałeś
// /Opt is [[US United States] [CA Canada] [MX Mexico]]:
// dopasowanie po wartości eksportowej, a /I staje się [1]
Pdf.SetFormFieldValue('Country', 'CA');
// Edytowalne kombi z wartością spoza /Opt: /V jest zapisywane,
// /I jest usuwane, a żaden indeks nie jest wymyślany
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Wartość i wygląd to dwie osobne operacje
SetFormFieldValue nigdy nie dotyka strumienia wyglądu pola tekstowego albo wyboru. Po wywołaniu /V trzyma nowy tekst, a /AP /N nadal maluje stary, i to, który z nich pokaże czytelnik, zależy od tego, czy słownik AcroForm niesie /NeedAppearances true zgodnie z §12.7.3.3 i czy czytelnik to respektuje. Jeśli potrzebujesz, żeby plik renderował nową wartość w każdym czytniku, także w spłaszczaczach i generatorach miniatur, które tę flagę ignorują, wywołaj EnsureLoadedFieldAppearanceStream z indeksem pola. Buduje on Form XObject z odziedziczonego łańcucha /DA, wyrównania /Q, układu comb z /MaxLen i wartości, rozwiązuje nazwany font przez zasoby /DR AcroForm, żeby font Type0 zachował swój własny font potomny, a nie zdegradował się do Helvetica, i zwraca True, gdy co najmniej jeden widget dostał strumień. Wersja SetFormFieldValue przyjmująca nazwę nie zwraca indeksu, więc pobierz go przez GetFormField, które zwraca THPDFLoadedFormField należący do ciebie i wymagający zwolnienia. Zestaw regresyjny dla zmiany z v2.752.1 mówi o tym podziale wprost: ustawia wartość, woła EnsureLoadedFieldAppearanceStream, a potem renderuje stronę i sprawdza, że piksele wewnątrz prostokąta widgetu się zmieniły, a piksele poza nim nie. Sprawdzenie, że /V się zmieniło, nie dowodzi niczego o tym, co zobaczy użytkownik
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Wpisz nową wartość do /AP, żeby czytniki ignorujące
// /NeedAppearances też ją pokazały
if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
raise Exception.Create('No widget rectangle to paint into');
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;
Granice warte poznania, zanim na tym zbudujesz
ReconcileLoadedButtonAppearanceStates bada lokalne /FT słownika, który zaadresowałeś, więc działa na rodzicu grupy radio albo na checkboxie noszącym własne /FT; widget dziecka zaadresowany osobno, z /FT tylko na rodzicu, nie jest uzgadniany tą ścieżką. HPDFReconcileChoiceSelection obsługuje pojedynczą wartość skalarną i zapisuje co najwyżej jeden indeks; listy z wielokrotnym wyborem i kilkoma wybranymi pozycjami są poza tym, co SetFormFieldValue modeluje. Żadna z tych procedur nie waliduje wartości, którą przekazujesz, względem /Opt ani względem kluczy stanu włączenia, więc literówka daje checkbox Off albo kombi bez indeksu, a nie wyjątek. A GetFormFieldValue zwraca zapisany tekst /V tak, jak leży w słowniku, co dla wartości zakodowanej szesnastkowo oznacza zapis szesnastkowy, a nie zdekodowany tekst
Gdy wartości są już w środku, a wyglądy pomalowane, dwa naturalne następne kroki leżą po obu stronach tej operacji. Wymiana danych pól z systemami zewnętrznymi hurtowo, a nie jedno wywołanie SetFormFieldValue naraz, to temat importu i eksportu XFDF w Delphi. A gdy wypełniony formularz jest ostateczny i nie powinien być już edytowalny, spłaszczanie pól AcroForm i XFA w Delphi wpieka dokładnie te stany /AS i strumienie wyglądu opisane tutaj w statyczną treść strony, i dlatego doprowadzenie ich do spójności przed spłaszczeniem nie jest opcjonalne
API edycji wczytanych formularzy z tego artykułu, w tym SetFormFieldValue, EnsureLoadedFieldAppearanceStream i graf obliczeń przyrostowych, jest częścią HotPDF Delphi Component dla Delphi i C++Buildera