Checkboxy a radio buttony sa flatten-ujú ako nezaškrtnuté, pretože stav vzhľadu /AS nebol nikdy zosynchronizovaný s hodnotou poľa /V. PDFium Component, komponenta založená na PDFium pre VCL a LCL pre Delphi, C++Builder a Lazarus, teraz číta túto hodnotu pomocou FPDFAnnot_GetFormFieldValue, ktorá rieši slovník rodičovského poľa namiesto widget anotácie
Bug report, ktorý sem viedol, je typ, ktorému spočiatku neveríte. Zákazník flatten-uje podpísaný súhlasný formulár, otvorí výsledok, a každý checkbox je prázdny. Otvorte zdrojový súbor v Acrobate a boxy sú viditeľne zaškrtnuté. Prečítajte zdrojový súbor späť cez tú istú komponentu a hodnoty polí sú správne. Len flattenovaný výstup ich stráca, a len pre checkboxy a radio buttony: textové polia na tej istej stránke vyjdú v poriadku
Prečo sú checkboxy po flattenovaní nezaškrtnuté?
Pretože flattening sa nikdy nepozrie na /V. FPDFPage_Flatten zapečie widget appearance stream do obsahu stránky, a vzhľad, ktorý si vyberie, je ten pomenovaný podľa /AS. Ak /AS stále hovorí /Off, kým hodnota poľa hovorí, že box je zaškrtnutý, flattening verne zapečie off vzhľad. Hodnota nebola nikdy stratená; nikdy sa s ňou nekonzultovalo
ISO 32000-1 §12.5.5 definuje slovník vzhľadu /AP s tromi možnými záznamami, /N, /R a /D. Pre check box alebo radio button nie je záznam /N stream, ale pod-slovník, ktorého kľúče sú mená stavov vzhľadu, a §12.5.2 robí z /AS povinný selektor, keď je /N pod-slovník. Takže checkbox nesie dva predpripravené vzhľady a jeden ukazovateľ. Pomýliť sa v ukazovateli, a renderovanie je zlé spôsobom, ktorý žiadne množstvo správneho /V neopraví. Toto je tiež dôvod, prečo sa spôsob zlyhania líši od textových polí, ktoré nemajú žiadny predpripravený vzhľad, ktorý by sa dal vôbec vybrať: /N textového poľa je jeden stream, ktorý sa musí regenerovať od nuly po zmene hodnoty, takže GenerateFormAppearances spracúva tieto dva prípady cez úplne oddelené cesty kódu a pokazená bola len cesta pre tlačidlá
Kde skutočne žije hodnota checkboxu?
Na slovníku poľa, nie na widgete. ISO 32000-1 §12.7.5.2 popisuje check boxy a radio buttony ako button polia, ktorých /V je name objekt pomenúvajúci aktuálny stav vzhľadu, a §12.7.3.1 umiestňuje /V medzi záznamy spoločné pre všetky slovníky polí. Widget anotácia definovaná v §12.5.6.19 prispieva /AS a /AP. Nič v špecifikácii nenúti widget niesť /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 je chybná. Jej kontrakt je presne to, čo hovorí jej meno: vytiahnuť string záznam zo slovníka anotácie, ktorý ste jej odovzdali. Pýtať sa jej na /V na objekte 13 vráti nič, pretože objekt 13 skutočne nemá žiadne /V. Chyba bola vo volajúcom, ktorý predpokladal plochý objektový model, ktorý ISO 32000-1 nikdy nesľubovala
Kedy pole a widget zdieľajú jeden slovník?
Vždy, keď má pole presne jeden widget. §12.5.6.19 povoľuje, aby sa slovník poľa a jeho jediná widget anotácia zlúčili do jedného objektu, a väčšina autorských nástrojov túto skratku využíva. V zlúčenom objekte sedia /FT, /T, /V, /AS a /AP vedľa seba, takže čítanie /V na úrovni widgetu uspeje a celý bug ostane neviditeľný
V momente, keď pole vlastní dva alebo viac widgetov, je zlúčenie nemožné, a §12.7.3.1 vyžaduje, aby sa widgety stali /Kids samostatného slovníka poľa. Každá radio skupina je v tomto tvare z podstaty. Rovnako sú aj súhlasné checkboxy opakované v hlavičke a pätičke, a akékoľvek pole, ktoré autorský nástroj skopíroval na druhú stránku. To je celé vysvetlenie, prečo chyba prežila regresnú sadu: testovací korpus bol plný jednowidgetových formulárov a súbory zákazníka neboli. Ak prechádzate widgety sami namiesto spoliehania sa na komponentu, rovnaká asymetria sa objaví v poradí enumerácie, a poznámky o navigácii polí formulára PDF s PDFium Component pokrývajú, ako súvisí prechod anotáciami na úrovni stránky so stromom polí na úrovni dokumentu
Čítanie hodnoty spôsobom, aký PDFium zamýšľa
FPDFAnnot_GetFormFieldValue je správne API, a bolo v komponente naviazané už nejaký čas bez toho, aby ho cesta pre checkboxy používala. Berie form handle aj anotáciu, čo je signál, na ktorom záleží: s dostupným form-fill prostredím PDFium vyrieši anotáciu na jej form control a prečíta hodnotu z objektu poľa, takže vráti správnu odpoveď pre zlúčené aj rozdelené rozloženia
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;
Dva detaily v tomto úryvku sa dajú ľahko pomýliť. Vrátená dĺžka je bajtový počet pre UTF-16 text vrátane terminátora, takže počet znakov je buflen div 2 - 1, a hodnota 2 znamená prázdny reťazec. Guard buflen >= 4 teda znamená aspoň jeden skutočný znak, čo je to, čo drží pole bez akéhokoľvek /V, aby sa jeho /AS neprepísalo prázdnym menom
Na čom sa /AS a /AP /N skutočne zhodujú?
Zhodujú sa na mene, a meno vyberá ten, kto súbor vyprodukoval. §12.7.5.2 vyžaduje, aby sa off stav volal /Off, a on stav ponecháva úplne na producentovi. /Yes je konvencia, nie pravidlo. Acrobat zapisuje /Yes, ale mnoho generátorov zapisuje /On, /1, /Choice1, alebo lokalizované slovo, a radio skupina zvyčajne dáva každému kidovi odlišné meno on-stavu, aby skupina mohla vyjadriť, ktoré tlačidlo je vybraté. Toto je presne dôvod, prečo je kopírovanie /V doslovne do /AS správna operácia, nie hack: pre zaškrtnutý control PDFium hlási meno on-stavu, ktoré definuje sám súbor, a pre nezaškrtnutý hlási Off, takže hodnota, ktorú zapíšete do /AS, je garantovane kľúč, ktorý existuje v pod-slovníku /AP /N daného widgetu. Hardcodovanie /Yes by fungovalo na výstupe Acrobatu a potichu by sa pokazilo všade inde
Poradie operácií a kde je stále potrebná opatrnosť
Sekvencia je pevná a neodpúšťajúca: zapnúť form fill, priradiť hodnoty, regenerovať vzhľady, flattenovať, potom uložiť. Preskočte krok regenerácie, a FPDFPage_Flatten nájde prázdne alebo zastarané appearance streamy a zapečie ich bez sťažnosti, čo je tichá strata dát, nie chybový návrat
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');
Zostávajú dve poctivé hranice. Po prvé, synchronizácia zapíše hodnotu poľa do /AS každého widgetu tohto poľa, čo je správne pre checkboxy a približné pre radio skupiny, ktorých kidy si každý definujú vlastné meno on-stavu; kid, ktorého /AP /N nemá žiadny záznam zodpovedajúci zapísanému /AS, nemá žiadny vzhľad na výber podľa §12.5.5, takže nevybrané tlačidlo sa môže flattenovať na nič namiesto prázdneho kruhu. Auditovanie radio skupiny pomocou FPDFAnnot_GetFormControlIndex pred flattenovaním stojí za pár riadkov navyše. Po druhé, nič z toho sa netýka XFA, kde hodnota žije v XML dátovom pakete namiesto v slovníkoch AcroForm, čo je oddelenie pokryté v poznámkach o úpravách polí XFA, ktoré sa nepersistujú. Všeobecné poučenie sa oplatí ponechať si aj po tejto jednej oprave: kedykoľvek API berie form handle popri anotácii, hovorí vám, že vyrieši hierarchiu poľa za vás, a kedykoľvek berie len anotáciu, prečíta presne objekt, ktorý ste odovzdali. Toto rozlíšenie tiež riadi výmenu dát, keďže export a import dát formulára XFDF pracuje v plne kvalifikovaných menách polí, nikdy v pozíciách widgetov
Flattenovanie formulára je jedna z tých funkcií, ktorá vyzerá ako jediné API volanie a ukáže sa byť kontraktom medzi tromi slovníkmi. Ak by ste radšej pracovali proti komponente, ktorá tento kontrakt už kóduje, PDFium Component pre Delphi a C++Builder dodáva regeneráciu vzhľadu, flattenovanie a prístup k poliam formulára tu popísaný ako obyčajné vlastnosti a metódy