Odborný článok

PDF Checkbox Flatten bug: Field Value vs Widget v Delphi

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