Checkboxy i przyciski radiowe spłaszczają się jako odznaczone, ponieważ stan wyglądu /AS nigdy nie był synchronizowany z wartością pola /V. PDFium Component, komponent VCL i LCL oparty na PDFium dla Delphi, C++Buildera i Lazarusa, odczytuje teraz tę wartość funkcją FPDFAnnot_GetFormFieldValue, która rozwiązuje nadrzędny słownik pola zamiast widgetu adnotacji
Zgłoszenie błędu, które doprowadziło do tego artykułu, jest z tych, którym najpierw nie ufasz. Klient spłaszcza podpisany formularz zgody, otwiera wynik, a każdy checkbox jest pusty. Otwórz plik źródłowy w Acrobacie, a pola są widocznie zaznaczone. Odczytaj plik źródłowy z powrotem przez ten sam komponent, a wartości pól są poprawne. Tylko spłaszczony wynik je traci, i tylko dla checkboxów i przycisków radiowych: pola tekstowe na tej samej stronie wychodzą poprawnie
Dlaczego checkboxy są odznaczone po spłaszczeniu?
Ponieważ spłaszczanie nigdy nie zagląda do /V. FPDFPage_Flatten wypieka strumień wyglądu widgetu w treść strony, a wygląd, który wybiera, to ten nazwany przez /AS. Jeśli /AS wciąż mówi /Off, podczas gdy wartość pola mówi, że pole jest zaznaczone, spłaszczanie wiernie wypieka wygląd off. Wartość nigdy nie została utracona; po prostu nigdy nie została sprawdzona
ISO 32000-1 §12.5.5 definiuje słownik wyglądu /AP z trzema możliwymi wpisami: /N, /R i /D. Dla checkboxa lub przycisku radiowego wpis /N to nie strumień, lecz podsłownik, którego kluczami są nazwy stanów wyglądu, a §12.5.2 czyni /AS wymaganym selektorem, gdy /N jest podsłownikiem. Checkbox niesie więc dwa gotowe wyglądy i jeden wskaźnik. Pomyl wskaźnik, a renderowanie będzie błędne w sposób, którego żadna ilość poprawnego /V nie naprawi. To także powód, dla którego tryb awarii różni się od pól tekstowych, które w ogóle nie mają gotowego wyglądu do wyboru: /N pola tekstowego to pojedynczy strumień, który musi być regenerowany od zera po zmianie wartości, więc GenerateFormAppearances obsługuje te dwa przypadki zupełnie osobnymi ścieżkami kodu, a zepsuta była tylko ścieżka przycisków
Gdzie faktycznie żyje wartość checkboxa?
W słowniku pola, nie w widgecie. ISO 32000-1 §12.7.5.2 opisuje checkboxy i przyciski radiowe jako pola przyciskowe, których /V jest obiektem nazwy określającym bieżący stan wyglądu, a §12.7.3.1 umieszcza /V wśród wpisów wspólnych dla wszystkich słowników pól. Widget adnotacji zdefiniowany w §12.5.6.19 dostarcza /AS i /AP. Nic w specyfikacji nie zobowiązuje widgetu do niesienia /V
// Wrong: reads the widget annotation dictionary directly
buflen := FPDFAnnot_GetStringValue(Annot, 'V', nil, 0);
// For most real forms buflen comes back as 2 (an empty UTF-16 string),
// so /AS is never written and the box flattens as Off
{ What the two objects look like when the field has several widgets:
12 0 obj % field dictionary (the parent)
<< /FT /Btn /T (Consent) /V /On
/Kids [ 13 0 R 14 0 R ] >>
endobj
13 0 obj % widget annotation (a kid)
<< /Type /Annot /Subtype /Widget /Parent 12 0 R
/AS /Off
/AP << /N << /On 20 0 R /Off 21 0 R >> >> >>
endobj }
FPDFAnnot_GetStringValue nie jest wadliwa. Jej kontrakt jest dokładnie taki, jak mówi nazwa: pobrać wpis łańcuchowy ze słownika adnotacji, który jej przekazałeś. Pytanie jej o /V na obiekcie 13 zwraca nic, bo obiekt 13 rzeczywiście nie ma /V. Defekt leżał w kodzie wywołującym, który zakładał płaski model obiektowy, jakiego ISO 32000-1 nigdy nie obiecywało
Kiedy pole i widget współdzielą jeden słownik?
Zawsze wtedy, gdy pole ma dokładnie jeden widget. §12.5.6.19 pozwala scalić słownik pola i jego pojedynczy widget adnotacji w jeden obiekt, a większość narzędzi autorskich korzysta z tego skrótu. W scalonym obiekcie /FT, /T, /V, /AS i /AP siedzą obok siebie, więc odczyt /V na poziomie widgetu się powodzi, a cały błąd pozostaje niewidoczny
W chwili, gdy pole ma dwa lub więcej widgetów, scalenie staje się niemożliwe, a §12.7.3.1 wymaga, by widgety stały się /Kids osobnego słownika pola. Każda grupa przycisków radiowych ma tę postać z konstrukcji. Podobnie checkboxy zgody powtórzone w nagłówku i stopce, oraz każde pole skopiowane przez narzędzie autorskie na drugą stronę. To całe wyjaśnienie, dlaczego defekt przetrwał zestaw testów regresyjnych: korpus testowy był pełen formularzy z pojedynczym widgetem, a pliki klienta nie były. Jeśli sam przechodzisz przez widgety zamiast polegać na komponencie, ta sama asymetria pojawia się w kolejności wyliczania, a notatki o nawigacji po polach formularza PDF w PDFium Component omawiają, jak przejście po adnotacjach na poziomie strony odnosi się do drzewa pól na poziomie dokumentu
Odczytywanie wartości tak, jak zamierza PDFium
FPDFAnnot_GetFormFieldValue jest poprawnym API i była związana w komponencie od jakiegoś czasu, zanim ścieżka checkboxa zaczęła jej używać. Przyjmuje uchwyt formularza obok adnotacji, co jest sygnałem, który ma znaczenie: mając dostępne środowisko wypełniania formularza, PDFium rozwiązuje adnotację do jej kontrolki formularza i odczytuje wartość z obiektu pola, więc zwraca poprawną odpowiedź zarówno dla scalonych, jak i podzielonych układów
FPDF_FORMFIELD_CHECKBOX, FPDF_FORMFIELD_RADIOBUTTON:
begin
// /AP is prebuilt per state; only /AS has to be synchronised with /V.
// FPDFAnnot_GetFormFieldValue resolves the parent field dictionary,
// which is where ISO 32000-1 12.7.5.2 keeps the value.
buflen := FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, nil, 0);
if buflen >= 4 then
begin
SetLength(OrigVal, buflen div 2 - 1);
FPDFAnnot_GetFormFieldValue(FFormHandle, Annot, PWideChar(OrigVal), buflen);
FPDFAnnot_SetStringValue(Annot, 'AS', Pointer(OrigVal));
end;
end;
Dwa szczegóły w tym fragmencie łatwo pomylić. Zwrócona długość to liczba bajtów tekstu UTF-16 wliczając terminator, więc liczba znaków to buflen div 2 - 1, a wartość 2 oznacza pusty ciąg. Warunek buflen >= 4 oznacza więc co najmniej jeden prawdziwy znak, co powstrzymuje pole bez żadnego /V przed nadpisaniem jego /AS pustą nazwą
Na czym właściwie zgadzają się /AS i /AP /N?
Zgadzają się na nazwę, a nazwę wybiera ten, kto wyprodukował plik. §12.7.5.2 wymaga, by stan off nazywał się /Off, a stan on pozostawia całkowicie producentowi. /Yes to konwencja, nie reguła. Acrobat zapisuje /Yes, ale mnóstwo generatorów zapisuje /On, /1, /Choice1 lub zlokalizowane słowo, a grupa przycisków radiowych zwykle nadaje każdemu dziecku odrębną nazwę stanu on, tak by grupa mogła wyrazić, który przycisk jest wybrany. To dokładnie dlatego kopiowanie /V dosłownie do /AS jest poprawną operacją, a nie hackiem: dla kontrolki zaznaczonej PDFium zgłasza nazwę stanu on, którą sam plik definiuje, a dla niezaznaczonej zgłasza Off, więc wartość, którą zapisujesz do /AS, ma gwarancję bycia kluczem istniejącym w podsłowniku /AP /N tego widgetu. Zaszycie na sztywno /Yes działałoby na wyjściu Acrobata i po cichu zawodziło wszędzie indziej
Kolejność operacji i gdzie wciąż trzeba zachować ostrożność
Sekwencja jest ustalona i bezlitosna: włącz wypełnianie formularza, przypisz wartości, zregeneruj wyglądy, spłaszcz, a potem zapisz. Pomiń krok regeneracji, a FPDFPage_Flatten znajdzie puste lub nieaktualne strumienie wyglądu i wypiecze je bez żadnego zastrzeżenia, co jest cichą utratą danych, a nie zwróconym błędem
Pdf.FileName := FormPath;
Pdf.FormFill := True; // required: FormHandle must exist
Pdf.Active := True;
Pdf.FormField[0] := 'On'; // writes /V only
Pdf.GenerateFormAppearances; // syncs /AS for buttons, rebuilds /AP for text
if Pdf.FlattenAllPages(FLAT_PRINT) then
Pdf.SaveAs('consent-flat.pdf');
Pozostają dwa uczciwe ograniczenia. Po pierwsze, synchronizacja zapisuje wartość pola do /AS każdego widgetu tego pola, co jest poprawne dla checkboxów, ale przybliżone dla grup przycisków radiowych, których każde dziecko definiuje własną nazwę stanu on; dziecko, którego /AP /N nie ma wpisu pasującego do zapisanego /AS, nie ma wyglądu do wybrania zgodnie z §12.5.5, więc niewybrany przycisk może spłaszczyć się do niczego zamiast do pustego kółka. Audytowanie grupy przycisków radiowych funkcją FPDFAnnot_GetFormControlIndex przed spłaszczeniem jest warte tych kilku linii. Po drugie, nic z tego nie dotyczy XFA, gdzie wartość żyje w pakiecie danych XML, a nie w słownikach AcroForm, co jest osobnym zagadnieniem omówionym w notatkach o edycjach pól XFA, które nie są trwale zapisywane. Warto zachować lekcję ogólną wykraczającą poza tę jedną poprawkę: ilekroć API przyjmuje uchwyt formularza obok adnotacji, mówi ci, że rozwiąże za ciebie hierarchię pola, a ilekroć przyjmuje wyłącznie adnotację, odczyta dokładnie ten obiekt, który przekazałeś. To rozróżnienie rządzi też wymianą danych, ponieważ eksportowanie i importowanie danych formularza XFDF działa na w pełni kwalifikowanych nazwach pól, nigdy na pozycjach widgetów
Spłaszczanie formularza to jedna z tych funkcji, która wygląda jak pojedyncze wywołanie API, a okazuje się kontraktem między trzema słownikami. Jeśli wolisz pracować z komponentem, który już koduje ten kontrakt, PDFium Component dla Delphi i C++Buildera dostarcza regenerację wyglądu, spłaszczanie i dostęp do pól formularza opisane tutaj jako zwykłe właściwości i metody