HotPDF Delphi Component traktuje /FT, /Ff, /V i /DV na wczytanym polu AcroForm jako atrybuty dziedziczne, rozwiązywane przez spacer po łańcuchu /Parent. Od v2.754.3 i v2.754.4 nazwane dziecko, którego typ pochodzi od rodzica, pozostaje indywidualnie adresowalne, RemoveFormField zostawia jego rodzeństwo w spokoju, a ResetLoadedFormField kopiuje odziedziczony domyślny z jego oryginalnym typem obiektu PDF. Wcześniej zaskakująco dużo zwyczajnych formularzy było błędnie czytanych
Formularz, który to wszystko odsłania, nie jest egzotyczny. Narzędzie autorskie buduje węzeł grupy group, który niesie raz /FT /Ch, flagi pola i listę opcji, a pod nim wiesza dwoje nazwanych dzieci a i b, każde złączony słownik pole-plus-widget z niczym poza /T, /Parent, /Rect i własnym /V. To całkowicie legalny sposób współdzielenia atrybutów i dokładnie ten przypadek, który sekcja Limits artykułu o ustawianiu wartości pól formularza we wczytanym PDF w Delphi oznaczyła jako nieobsłużony: uzgadnianie przycisków patrzyło tylko na lokalne /FT. Ten artykuł podchodzi tam, gdzie tamten się zatrzymał, obejmując klasyfikację drzewa pól, czytanie wartości odziedziczonych i to, co reset pojedynczego pola ma prawo zapisać
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, więc każdy czytnik patrzący tylko na lokalny słownik zgłosi zły typ, złe flagi i pustą wartość dla całkowicie poprawnego dziecka. HotPDF przepuszcza wszystkie te odczyty przez jeden wewnętrzny resolver, HPDFLoadedInheritedFieldObject, który sprawdza słownik pod kątem klucza, rozwiązuje referencję pośrednią, jeśli ją znajdzie, a w przeciwnym razie idzie za /Parent najwyżej 128 poziomów, bo zniekształcone pliki potrafią budować cykle /Parent nie mające nic wspólnego z /Kids. Na nim siedzą publiczne gettery: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue oraz helpery opcji GetLoadedFormFieldOptionCount i GetLoadedFormFieldOptions, które łapią też tablicę /Opt zapisaną na rodzicu. Jedna reguła w resolverze łatwo ucha: spacer zatrzymuje się na pierwszym słowniku zawierającym klucz, nawet jeśli wartością jest tam pusty łańcuch. Lokalne /V () to celowe przysłonięcie rodzica, a nie luka do wypełnienia z wyższych pięter drzewa
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;
Dlaczego lokalne /FT to zły test na pole terminalne?
Bo rodzic może podawać typ i nadal posiadać nazwane pola dzieci, więc obecność /FT nie mówi nic o tym, gdzie kończy się drzewo pól. Stare przejście ogłaszało węzeł terminalnym, gdy tylko miał własne /FT albo brak /Kids. W formularzu wyżej group ma i /FT /Ch, i /Kids, więc był rejestrowany jako jedno pole o nazwie group z dwoma widgetami, a pełne nazwy kwalifikowane group.a i group.b po prostu znikały. GetFormFieldCount zwracał 1, wyszukanie po nazwie dziecka padało, a SetFormFieldValue mógł pisać tylko do współdzielonego rodzica. Zastępczy test, HPDFLoadedFieldHasChildFields, patrzy na dzieci zamiast na rodzica: element /Kids jest polem-dzieckiem, jeśli ma własne /T, ma własne /Kids albo w ogóle nie jest słownikiem /Subtype /Widget. Tylko gdy żadne dziecko się nie kwalifikuje, węzeł jest terminalny, a jego dzieci traktowane są jako jego adnotacje widget
Oba przypadki brzegowe, które ukształtowały tę regułę, wychodzą ze słowników złączonych, na jakie §12.7.3.1 pozwala, gdy pole ma pojedynczy widget. Nazwany złączony słownik niesie /Subtype /Widget i nadal jest polem-dzieckiem, więc sam subtyp nie może go wysłać do anonimowej listy widgetów rodzica; wygrywa /T. Odwrotność też się zdarza: część producentów powtarza /FT rodzica na każdym anonimowym widgecie, więc /FT nie może służyć jako dowód, że widget rozpoczyna nowe pole. Klasyfikację współdzielą cache relacji, FormFieldExists i RemoveFormField, a każde z tych przejść zapisuje teraz odwiedzone słowniki i zatrzymuje się po 128 poziomach. Plik regresyjny, którego grupa wypisuje siebie dwa razy, /Kids [5 0 R 5 0 R 6 0 R 7 0 R], nadal raportuje dokładnie dwa pola zamiast rekursować w nieskończoność albo liczyć ten sam węzeł podwójnie
Jak RemoveFormField unika usuwania pól rodzeństwa?
RemoveFormField usuwa teraz tylko nazwane przez ciebie dziecko, bo odkrywanie i usuwanie wreszcie zgodziły się co do tego, czym jest pole terminalne. Ta zgoda znaczy więcej, niż wygląda. Wersja po nazwie rozwiązuje indeks przez cache relacji, a potem liczy pola terminalne w drugim przejściu po /AcroForm /Fields. Gdy tylko cache naprawiono, by widział group.a i group.b, nienaprawione przejście usuwające nadal traktowałoby group jako pojedyncze pole terminalne, a indeks 0 usunąłby rodzica razem z całym rodzeństwem i wszystkimi ich widgetami. Przejście usuwające używa teraz tego samego testu HPDFLoadedFieldHasChildFields i tego samego zbioru odwiedzonych, zbiera adnotacje widget usuniętego dziecka i tylko jego, zdejmuje je z /Annots każdej strony, a rodzica usuwa tylko wtedy, gdy jego tablica /Kids zostaje pusta. Regresja sprawdza wszystkie trzy miejsca, gdzie błąd by wyszedł: /Kids rodzica, /Annots strony oraz wartość i wygląd przeżywszego rodzeństwa, zarówno po pełnym przepisaniu, jak i po aktualizacji przyrostowej
// Usuń jedno nazwane dziecko; jego rodzeństwo i współdzielony rodzic przeżyją
Pdf.RemoveFormField('group.a');
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');
Co zapisuje ResetLoadedFormField, gdy domyślny jest dziedziczony?
ResetLoadedFormField zapisuje lokalne /V będące świeżą kopią odziedziczonego /DV z tym samym typem obiektu PDF i waliduje cały domyślny, zanim dotknie pola. Typ obiektu ma znaczenie, bo skalarne gettery spłaszczają wszystko do tekstu. Domyślny checkboxa to nazwa typu /Yes, domyślny listy wielokrotnego wyboru to tablica łańcuchów, a domyślny tekstowy może być szesnastkowym łańcuchem UTF-16; skopiowanie któregokolwiek przez GetLoadedFormFieldDefaultValue zamieniłoby nazwę w łańcuch, tablicę w pusty łańcuch, a łańcuch szesnastkowy w jego literalne cyfry. Reset rozgałęzia się więc po odziedziczonym typie: pola tekstowe i wyboru dostają nowy obiekt łańcucha zachowujący flagę IsHexadecimal, pola wyboru z domyślnym tablicowym dostają nową tablicę nowych łańcuchów, a przyciski inne niż pushbutton dostają nowy obiekt nazwy. Kopiowanie, zamiast wskazywać na obiekty rodzica, jest celowe: /V współdzielące tablicę /DV rodzica albo jej numer obiektu zmieniłoby domyślny, gdy ktokolwiek następnym razem edytowałby wartość. Domyślny złego typu albo tablica wyboru zawierająca cokolwiek poza łańcuchami rzuca wyjątek i zostawia /V oraz /I dokładnie takie, jakie były. Pushbuttony, które nie mają wartości (Tabela 226, bit 17), i pola podpisu schodzą na starszą ścieżkę tylko-łańcuchową
Gdy nie ma /DV nigdzie w górę łańcucha, metoda dotrzymuje swojej umowy czyszczenia, zapisując lokalny pusty łańcuch, albo /Off dla checkboxa albo pola radio. Skasowanie lokalnego /V wyglądałoby schludniej i byłoby błędem: rodzic może trzymać aktualną wartość, a zdjęcie przysłonięcia dziecka po cichu przywróciłoby tę wartość. Dlatego też reset pojedynczego pola to nie akcja ResetForm z §12.7.5.3, którą przeglądarka puszcza na zbiorze pól, gdy użytkownik kliknie przycisk, jak opisano w artykule o budowaniu pól i akcji AcroForm w HotPDF. ResetLoadedFormField to operacja edycyjna na jednym wczytanym polu, z własną regułą na przypadek braku domyślnego, i zapisuje pole przez NoteLoadedFormFieldDirty, żeby przyrostowe przeliczanie widziało zmianę
var
Field: THPDFLoadedFormField;
begin
Field := Pdf.GetFormField('group.a');
try
// Rodzic trzyma /DV [(b) (r)] na liście MultiSelect: group.a dostaje
// własne /V [(b) (r)] i świeże /I [0 2]; rodzic nietknięty
Pdf.ResetLoadedFormField(Field.Index);
// Skalarne gettery nie umieją przedstawić domyślnego tablicowego
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // pusty
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Zgoda między /V, /I i /AS
Reset jest poprawny tylko wtedy, gdy indeks wyboru i stan wyglądu idą za wartością, więc ResetLoadedFormField kończy się tymi samymi dwoma uzgadniaczami co SetFormFieldValue. HPDFReconcileChoiceSelection przyjmuje teraz wartość tablicową: usuwa lokalne /I bez mutowania go, dopasowuje każdą wartość do eksportowej połowy każdego wpisu /Opt i zapisuje jedno nowe sortowane /I, więc reset do [(b) (r)] przy opcjach b, g, r daje /I [0 2]. ReconcileLoadedButtonAppearanceStates pyta teraz o typ odziedziczony, więc checkbox dziecko, którego /FT /Btn mieszka na rodzicu, wreszcie dostaje ustawione 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 porządne /Type /XObject, /Subtype /Form i /BBox; przed v2.754.4 odtworzenie wyglądu po resecie mogło zaznaczyć checkbox od nowa, zanim plik został zapisany
Granice warte poznania, zanim na tym zbudujesz
Skalarne gettery pozostają skalarne. 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. Cykl /Parent kończy spacer bez wyjątku, więc pole, którego typ przepadł w cyklu, raportuje lfftUnknown i flagi 0, zamiast padać. SetFormFieldValue i ResetLoadedFormField zawsze zapisują dziecko, które zaadresujesz, i nigdy nie promują wartości do współdzielonego rodzica, co jest słuszne dla niezależnych dzieci, ale znaczy, że grupy radio należy zaadresować przez pole posiadające wybór. I każde wywołanie komituje jedno pole na własną rękę; nic tu nie czyni z partii resetów transakcji
Rozwiązywanie atrybutów dziedzicznych, ujednolicona klasyfikacja drzewa pól i typowany reset opisane tutaj są częścią API formularzy wczytanych w HotPDF Delphi Component dla Delphi i C++Buildera, obok tworzenia pól omówionego w artykule o dodawaniu pól AcroForm do wczytanego PDF w Delphi