Formularz ankiety pokazuje w Acrobat dwie listy rozwijane, group.a i group.b. Przejdź /AcroForm /Fields kodem, który traktuje każdy węzeł niosący własne /FT jako gotowe pole, a licznik wraca jako 1: pojedyncze pole nazwane group z dwoma widgetami. Poproś o group.b po nazwie, a wyszukiwanie pada. Otwórz słownik dziecka, a nie ma w nim w ogóle /FT, ani /Ff, ani listy opcji. Nic nie jest zepsute. Typ, flagi i opcje siedzą piętro wyżej na rodzicu, a specyfikacja PDF wprost pozwala im tam mieszkać
Układ jest legalny i oszczędny. Narzędzie autorskie zapisuje węzeł grupy z /FT /Ch, /Ff 131078 i tablicą /Opt raz, po czym wiesza pod nim dwoje nazwanych dzieci, każde złączony słownik pole-plus-widget niosący tylko /T, /Parent, /Rect i własne /V. Poprawne czytanie takiego formularza bierze trzy rzeczy: chodzenie za /Parent dla każdego klucza dziedzicznego, rozstrzyganie, gdzie drzewo pól naprawdę się kończy, i pilnowanie, by reset zapisywał wartość właściwego typu PDF. Ten tekst podchodzi tam, gdzie ustawianie wartości pól formularza we wczytanym PDF w Delphi się zatrzymało
Które wpisy AcroForm pole może odziedziczyć po rodzicu?
ISO 32000-1 §12.7.3.1 (Tabela 220) oznacza /FT, /Ff, /V i /DV jako dziedziczne, a Tabela 229 w §12.7.4.3 robi to samo z /MaxLen pola tekstowego. Tablice opcji dostają tu tego samego traktowania, bo producenci dzielący listę wyboru między dziećmi zapisują /Opt na grupie. Czytnik sprawdzający tylko lokalny słownik zgłosi więc zły typ, flagi 0 i pustą wartość dla dziecka, które jest w pełni poprawne
HotPDF Delphi Component odpowiada na każdy z tych odczytów tak samo. Szuka klucza w słowniku samego pola i, jeśli klucz tam jest, rozwiązuje referencję pośrednią i stopuje. W przeciwnym razie idzie za /Parent i próbuje znowu, najwyżej 128 poziomów. Ten limit nie jest ozdobnikiem. Wskaźniki /Parent w zniekształconych plikach potrafią utworzyć pętlę niemającą nic wspólnego z /Kids, a czytnik ufający im bezwarunkowo zawiesza się na pierwszym takim pliku
Jedna reguła w tym przejściu łatwo odwraca się na lewą stronę: wygrywa pierwszy słownik zawierający klucz, nawet gdy wartość tam zapisana to pusty łańcuch. Lokalne /V () na group.b to autor mówiący „to dziecko jest puste”, a przejście stopuje tam, zamiast wspinać się na wartość, którą może trzymać rodzic. Traktowanie pustki jako „braku” sprawiłoby, że wyczyszczone dziecko pokazywałoby wybór rodzica, co jest odwrotnością tego, co autor zapisał
Publiczne gettery wszystkie siedzą na tym wyszukiwaniu: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue oraz helpery opcji GetLoadedFormFieldOptionCount i GetLoadedFormFieldOptions. Czytanie dziecka grupy ankiety nie potrzebuje więc żadnego specjalnego kodu:
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' niesie /FT /Ch, /Ff 131078 i /Opt; dziecko
// 'group.b' niesie tylko /T, /Parent, /Rect i własne /V
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // lokalne /V
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
GetFormField zwraca za każdym razem nowy wrapper THPDFLoadedFormField i rzuca wyjątek, gdy nazwa jest nieznana: trzymaj wywołanie poza blokiem try wrappera i zwalniaj wrapper sam
Odnajdowanie, gdzie kończy się drzewo pól
Lokalne /FT to zły test na pole terminalne, bo rodzic może podawać typ i nadal posiadać nazwane pola dzieci. Kuszące przejście przestaje schodzić, gdy tylko węzeł ma własne /FT albo nie ma /Kids. W formularzu ankiety group ma i /FT /Ch, i /Kids, więc to przejście rejestruje go jako jedno pole z dwoma adnotacjami widget. Nazwy group.a i group.b znikają. Licznik pól wynosi 1. Wyszukiwanie po nazwie dziecka pada, a zapis wartości może wylądować tylko na współdzielonym rodzicu
HotPDF patrzy zamiast tego na dzieci. Dziecko liczy się jako pole-dziecko, jeśli ma własne /T, ma własne /Kids, albo nie jest słownikiem /Subtype /Widget. Tylko gdy żadne dziecko się nie kwalifikuje, węzeł jest terminalny, a jego dzieci są wtedy traktowane jako jego widgety. Oba przypadki brzegowe stojące za tą regułą wychodzą ze słowników złączonych, na jakie §12.7.3.1 pozwala, gdy pole ma dokładnie jeden widget:
- Nazwany złączony słownik niesie
/Subtype /Widgeti nadal jest polem-dzieckiem. Sam subtyp nie może go wepchnąć do anonimowej listy widgetów rodzica; decyduje/T - Część producentów powtarza
/FTrodzica na każdym anonimowym widgecie./FTna widgecie nie da się więc czytać też jako „tu zaczyna się nowe pole”
Ta sama klasyfikacja napędza indeks pól, FormFieldExists i RemoveFormField. Każde z tych przejść zapisuje też słowniki, które już odwiedziło, i poddaje się po 128 poziomach. Załóżmy, że grupa jest obiektem 5 i wymienia siebie dwa razy wśród własnych dzieci: /Kids [5 0 R 5 0 R 6 0 R 7 0 R]. Nadal raportuje dokładnie dwa pola, dzieci 6 i 7. Żadnej nieskończonej rekursji i żadnego zmyślonego trzeciego pola z samoodwołania
Jak RemoveFormField unika usuwania pól rodzeństwa?
RemoveFormField usuwa tylko dziecko, które nazwiesz, bo odkrywanie i usuwanie dzielą jedną definicję pola terminalnego. Przeciążenie po nazwie rozwiązuje indeks pola z listy pól, a potem przechodzi /AcroForm /Fields drugi raz, licząc pola terminalne, by znaleźć słownik do usunięcia. Gdyby drugie przejście użyło kuszącej reguły /FT, podczas gdy pierwsze użyło reguły opartej na dzieciach, indeksy rozjechałyby się: przejście usuwające widziałoby group jako pole 0, a usunięcie group.a wzięłoby rodzica, rodzeństwo i oba widgety
Przejście usuwające stosuje więc ten sam test pola-dziecka i ten sam zbiór odwiedzonych co odkrywanie. Zbiera adnotacje widget należące do usuwanego dziecka, zdejmuje z /Annots każdej strony tylko te, a rodzica usuwa tylko wtedy, gdy jego tablica /Kids zostaje pusta. Przy group.b wciąż na liście rodzic zostaje, a typ, flagi i opcje, które group.b dziedziczy, zostają z nim
// Usuń jedno nazwane dziecko; jego rodzeństwo i współdzielony rodzic przeżyją
Pdf.RemoveFormField('group.a');
// Kontrole zdrowotne, które dodałbyś sam, nie część biblioteki
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Typ, flagi i opcje są nadal rozwiązywane przez rodzica
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Gdy sprawdzasz usunięcie na własnych formularzach, patrz w trzy miejsca, bo każde łapie inną pomyłkę: /Kids rodzica, /Annots strony oraz wartość i wygląd przeżywszego rodzeństwa. Zrób to raz po SaveLoadedDocument i raz po SaveIncrementalUpdate, bo zapis przyrostowy dokleja tylko zmienione obiekty, a /Kids rodzica muszą być wśród nich
Co ResetLoadedFormField kopiuje z dziedziczonego /DV
ResetLoadedFormField zapisuje lokalne /V będące świeżą kopią dziedziczonego /DV z tym samym typem obiektu PDF i waliduje kompletny domyślny, zanim dotknie pola. Domyślny checkboxa to nazwa typu /Yes. Domyślny listy wielokrotnego wyboru to tablica łańcuchów. Domyślny tekstowy może być szesnastkowym łańcuchem UTF-16 typu <FEFF...>. Przepuść którekolwiek z nich przez GetLoadedFormFieldDefaultValue i SetFormFieldValue, a dostaniesz łańcuch tam, gdzie powinna być nazwa, pusty łańcuch tam, gdzie była tablica, albo literalne cyfry szesnastkowe
Reset rozgałęzia się więc po tym, czym dziedziczony /DV faktycznie jest:
- Pola tekstowe i wyboru z łańcuchowym domyślnym dostają nowy obiekt łańcucha zachowujący flagę szesnastkową oryginału
- Pola wyboru z tablicowym domyślnym dostają nową tablicę nowych łańcuchów; tablica zawierająca cokolwiek poza łańcuchami rzuca wyjątek
- Checkboxy i przyciski radio dostają nowy obiekt nazwy
- Domyślny dowolnego innego typu rzuca wyjątek, a
/Vi/Izostają dokładnie takie, jakie były
Kopiowanie zamiast referencjonowania jest celowe i to jedyna bezpieczna opcja. Dziecięce /V wskazujące na tablicę /DV rodzica współdzieliłoby jeden obiekt między „wartość bieżącą” a „wartość domyślną”; następne SetFormFieldValue na tym dziecku po cichu przepisałoby domyślny dla każdego rodzeństwa. Kilka dodatkowych bajtów na polu resetowanym to tania cena za rozdzielenie tych dwóch. Pushbuttony, które nie mają wartości (Tabela 226, bit 17), i pola podpisu zostają na prostej ścieżce zapisującej łańcuchową kopię własnego /DV
Gdy nie ma /DV nigdzie w górę łańcucha, metoda zapisuje lokalny pusty łańcuch, albo /Off dla checkboxa albo pola radio. Skasowanie /V dziecka wyglądałoby schludniej i dałoby złą odpowiedź. Jeśli rodzic trzyma wartość bieżącą, zdjęcie przysłonięcia dziecka przywraca tę wartość wprost, a pole „zresetowane” pokazuje teraz coś, co użytkownik nigdy w nie nie wpisał
var
Field: THPDFLoadedFormField;
begin
// Rodzic 'palette' to lista MultiSelect z
// /Opt [(b) (g) (r)] i /DV [(b) (r)]; dziecko trzyma tylko /T i /V
Field := Pdf.GetFormField('palette.primary');
try
try
Pdf.ResetLoadedFormField(Field.Index);
// Dziecko ma teraz własne /V [(b) (r)] i /I [0 2];
// tablica /DV rodzica nietknięta
except
on E: Exception do
// Zniekształcony domyślny (zły typ, element tablicy nie-łańcuch):
// /V i /I zostały takie, jakie były
Writeln('Reset skipped: ', E.Message);
end;
// Skalarne getter nie umie przedstawić domyślnego tablicowego
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // pusty
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Reset pojedynczego pola to też co innego niż akcja ResetForm z §12.7.5.3, którą przeglądarka puszcza na zbiorze pól, gdy użytkownik kliknie przycisk; tę akcję omawia budowanie pól i akcji AcroForm w HotPDF. ResetLoadedFormField edytuje jedno wczytane pole, trzyma własną regułę dla przypadku bez domyślnego i oznacza pole jako zmienione, by przyrostowe przeliczanie je podchwyciło
Zgoda między /V, /I i /AS
Reset jest poprawny tylko wtedy, gdy indeks wyboru i stan wyglądu idą za wartością, a ResetLoadedFormField kończy się tymi samymi dwoma krokami uzupełniającymi co SetFormFieldValue. Dla pól wyboru zdejmuje lokalne /I dziecka, dopasowuje każdą wartość do eksportowej połowy każdego wpisu /Opt i zapisuje nowe sortowane /I. Reset do [(b) (r)] przy opcjach b, g, r daje /I [0 2]. Dziedziczone /I rodzica, jeśli jest, zostaje nietknięte
Przyciski pracują od typu odziedziczonego. Dziecięcy checkbox, którego /FT /Btn mieszka na rodzicu, nadal dostaje uaktualnione swoje /AS. Po stronie zapisu SetFormFieldValue i SetLoadedFormFieldDefaultValue przechowują obiekt nazwy dla odziedziczonego przycisku innego niż pushbutton, nawet gdy dziecko nie ma lokalnego wpisu, z którego można skopiować typ. A gdy EnsureLoadedFieldAppearanceStream przebudowuje wyglądy przycisków, zapisuje /AS /Off, chyba że wartość pasuje do stanu włączenia, i daje każdemu strumieniowi stanu /Type /XObject, /Subtype /Form i /BBox. Bez tej kontroli /Off regeneracja wyglądów po resecie pokazałaby box zaznaczony od nowa, zanim plik w ogóle zostanie zapisany
Skalarne gettery i inne krawędzie, które warto uwzględnić
Gettery łańcuchowe pozostają skalarne, a kod porównujący ich wyjście powinien wiedzieć, co spłaszczają. GetFormFieldValue i GetLoadedFormFieldDefaultValue zwracają pusty łańcuch dla wartości tablicowej, sprowadzają liczby i booleany do 42 albo true i raportują łańcuch zakodowany szesnastkowo w jego zapisie szesnastkowym. Jeśli musisz wiedzieć, czy domyślny listy trzyma dwa wybory albo żaden, getter domyślnego nie potrafi ci tego powiedzieć: oba wracają jako pusty łańcuch
Cykl /Parent kończy wyszukiwanie bez rzucenia, więc pole, którego typ przepadł w cyklu, raportuje lfftUnknown i flagi 0. Traktuj lfftUnknown na nazwanym polu jako znak pliku zniekształconego, wartego zalogowania. SetFormFieldValue i ResetLoadedFormField zawsze zapisują dziecko, które zaadresujesz, i nigdy nie promują wartości do współdzielonego rodzica. To właściwe dla niezależnych dzieci, takich jak dwie listy rozwijane ankiety. Grupy radio są inne: wybór należy do pola posiadającego grupę, więc adresuj to pole, a nie jedno z jego dzieci
Każde wywołanie komituje też jedno pole na własną rękę. Pętla resetów po dwudziestu polach to dwadzieścia niezależnych edycji; jeśli piąta rzuci na zniekształconym domyślnym, pierwsze cztery są już naniesione. Dla pracy wsadowej przeczytaj najpierw typ i domyślny każdego celu, zdecyduj, które pola dotkniesz, i dopiero wtedy zaczynaj pisać
Wyszukiwanie atrybutów dziedzicznych, klasyfikacja drzewa pól oparta na dzieciach i typowany reset należą wszystkie do API formularzy wczytanych w HotPDF Delphi Component, które obejmuje też projekty Delphi i C++Builder potrzebujące dodać nowe pola do istniejącego dokumentu, jak pokazuje dodawanie pól AcroForm do wczytanego PDF w Delphi