Checkboxovi i radio dugmad se flatten-uju kao neobeleženi jer stanje izgleda /AS nikada nije bilo sinhronizovano sa vrednošću polja /V. PDFium Component, VCL i LCL komponenta bazirana na PDFium-u za Delphi, C++Builder, i Lazarus, sada čita tu vrednost pomoću FPDFAnnot_GetFormFieldValue, koja razrešava rečnik roditeljskog polja umesto widget anotacije
Prijava baga koja je dovela do ovoga je vrsta kojoj u prvi mah ne verujete. Korisnik flatten-uje potpisan formular saglasnosti, otvori rezultat, i svaki checkbox je prazan. Otvorite izvorni fajl u Acrobatu i kućice su vidljivo označene. Pročitajte izvorni fajl nazad kroz istu komponentu i vrednosti polja su tačne. Samo flatten-ovan izlaz ih gubi, i to samo za checkboxove i radio dugmad: tekstualna polja na istoj stranici ispadnu ispravno
Zašto su checkboxovi neobeleženi posle flatten-a?
Zato što flatten nikada ne gleda u /V. FPDFPage_Flatten ugrađuje tok izgleda widgeta u sadržaj stranice, a izgled koji bira je onaj imenovan sa /AS. Ako /AS i dalje kaže /Off dok vrednost polja kaže da je kućica uključena, flatten verno ugradi isključen izgled. Vrednost nikada nije izgubljena; nikada nije konsultovana
ISO 32000-1 §12.5.5 definiše rečnik izgleda /AP sa tri moguće stavke, /N, /R, i /D. Za checkbox ili radio dugme stavka /N nije tok, nego podrečnik čiji su ključevi imena stanja izgleda, a §12.5.2 čini /AS obaveznim selektorom kada je /N podrečnik. Dakle checkbox nosi dva unapred izgrađena izgleda i jedan pokazivač. Pogrešite pokazivač i renderovanje je pogrešno na način koji nikakva ispravna /V neće popraviti. Ovo je takođe razlog zašto se mod otkazivanja razlikuje od tekstualnih polja, koja nemaju uopšte unapred izgrađen izgled za biranje: /N tekstualnog polja je jedan tok koji se mora regenerisati od nule pošto se vrednost promeni, pa GenerateFormAppearances obrađuje ta dva slučaja kroz potpuno odvojene putanje koda, a samo je putanja za dugmad bila pokvarena
Gde zapravo živi vrednost checkboxa?
Na rečniku polja, ne na widgetu. ISO 32000-1 §12.7.5.2 opisuje checkboxove i radio dugmad kao polja dugmadi čiji je /V objekat imena koji imenuje trenutno stanje izgleda, a §12.7.3.1 stavlja /V među stavke zajedničke svim rečnicima polja. Widget anotacija definisana u §12.5.6.19 doprinosi sa /AS i /AP. Ništa u specifikaciji ne obavezuje widget da nosi /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 nije neispravan. Njegov ugovor je tačno ono što mu ime kaže: dohvati stavku string iz rečnika anotacije koji ste mu predali. Traženje /V na objektu 13 vraća ništa jer objekat 13 zaista nema /V. Defekt je bio u pozivaocu, koji je pretpostavio ravan model objekata koji ISO 32000-1 nikada nije obećao
Kada polje i widget dele jedan rečnik?
Kad god polje ima tačno jedan widget. §12.5.6.19 dozvoljava da se rečnik polja i njegova jedina widget anotacija spoje u jedan objekat, a većina alata za autorizaciju koristi tu prečicu. U spojenom objektu /FT, /T, /V, /AS, i /AP svi sede jedan pored drugog, pa čitanje /V na nivou widgeta uspeva, i ceo bag ostaje nevidljiv
U trenutku kada polje poseduje dva ili više widgeta, spajanje postaje nemoguće, a §12.7.3.1 zahteva da widgeti postanu /Kids odvojenog rečnika polja. Svaka radio grupa je u ovom obliku po konstrukciji. Isto su i checkboxovi saglasnosti ponovljeni u zaglavlju i podnožju, i svako polje koje je alat za autorizaciju kopirao na drugu stranicu. To je celo objašnjenje zašto je defekt preživeo regresioni paket: korpus testova je bio pun formulara sa jednim widgetom, a korisnički fajlovi nisu bili. Ako sami obilazite widgete umesto da se oslanjate na komponentu, ista asimetrija se pojavljuje u redosledu nabrajanja, a beleške o navigaciji PDF form polja sa PDFium Component pokrivaju kako se obilazak anotacija na nivou stranice odnosi prema stablu polja na nivou dokumenta
Čitanje vrednosti na način na koji to PDFium namerava
FPDFAnnot_GetFormFieldValue je ispravan API, i bio je povezan u komponenti neko vreme bez da ga putanja checkboxa koristi. Uzima form handle uz anotaciju, što je signal koji je bitan: sa dostupnim okruženjem za popunjavanje formulara, PDFium razrešava anotaciju na njenu form kontrolu i čita vrednost iz objekta polja, pa vraća ispravan odgovor i za spojene i za razdvojene rasporede podjednako
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 detalja u tom isečku je lako pogrešno shvatiti. Vraćena dužina je broj bajtova za UTF-16 tekst uključujući terminator, pa je broj karaktera buflen div 2 - 1, a vrednost 2 znači prazan string. Čuvar buflen >= 4 zato znači bar jedan pravi karakter, što drži polje bez ikakvog /V da mu se /AS ne prepiše praznim imenom
Na čemu se /AS i /AP /N zapravo slažu
Slažu se na imenu, a ime bira ko god je proizveo fajl. §12.7.5.2 zahteva da se isključeno stanje zove /Off, i u potpunosti prepušta uključeno stanje proizvođaču. /Yes je konvencija, ne pravilo. Acrobat piše /Yes, ali mnogi generatori pišu /On, /1, /Choice1, ili lokalizovanu reč, a radio grupa obično daje svakom detetu posebno ime uključenog stanja tako da grupa može izraziti koje je dugme izabrano. Ovo je upravo razlog zašto je kopiranje /V bukvalno u /AS ispravna operacija, a ne hak: za označenu kontrolu PDFium prijavljuje ime uključenog stanja koje sam fajl definiše, a za neoznačenu prijavljuje Off, pa je vrednost koju upisujete u /AS garantovano ključ koji postoji u podrečniku /AP /N tog widgeta. Hardkodovanje /Yes bi radilo na izlazu iz Acrobata, a tiho bi otkazalo svuda drugde
Redosled operacija, i gde i dalje treba pažnja
Sekvenca je fiksna i neoprostiva: omogućiti popunjavanje formulara, dodeliti vrednosti, regenerisati izglede, flatten, zatim sačuvati. Preskočite korak regeneracije, i FPDFPage_Flatten pronalazi prazne ili zastarele tokove izgleda i ugrađuje ih bez primedbe, što je tih gubitak podataka, a ne povratna greška
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');
Dve iskrene granice ostaju. Prvo, sinhronizacija upisuje vrednost polja u /AS svakog widgeta tog polja, što je ispravno za checkboxove, a približno za radio grupe čija svaka deca definišu sopstveno ime uključenog stanja; dete čiji /AP /N nema stavku koja se poklapa sa upisanim /AS nema izgled za biranje pod §12.5.5, pa neizabrano dugme može flatten-ovati u ništa umesto u prazan krug. Provera radio grupe pomoću FPDFAnnot_GetFormControlIndex pre flatten-a vredi tih nekoliko linija. Drugo, ništa od ovoga ne važi za XFA, gde vrednost živi u XML paketu podataka umesto u AcroForm rečnicima, razdvajanje pokriveno u belešci o izmenama XFA polja koje se ne perzistuju. Opšta lekcija vredi zadržati posle ove pojedinačne popravke: kad god API uzima form handle uz anotaciju, govori vam da će razrešiti hijerarhiju polja umesto vas, a kad god uzima samo anotaciju, čitaće tačno objekat koji ste prosledili. Ta razlika takođe upravlja razmenom podataka, jer export i import XFDF podataka formulara radi sa potpuno kvalifikovanim imenima polja, nikad sa pozicijama widgeta
Flatten-ovanje formulara je jedna od onih funkcija koja izgleda kao pojedinačan API poziv, a ispostavi se da je ugovor između tri rečnika. Ako biste radije radili protiv komponente koja već enkodira taj ugovor, PDFium Component za Delphi i C++Builder isporučuje regeneraciju izgleda, flatten, i pristup form poljima opisan ovde kao obična svojstva i metode