Tehnički članak

Bug spljoštenja PDF checkboxa: vrijednost polja vs widget u Delphiju

Checkboxevi i radio gumbi spljošte se kao neoznačeni jer /AS nikad nije bio sinkroniziran s vrijednošću polja /V. PDFium Component, VCL i LCL komponenta temeljena na PDFium-u za Delphi, C++Builder i Lazarus, sada čita tu vrijednost pomoću FPDFAnnot_GetFormFieldValue, koja razrješava rječnik roditeljskog polja umjesto widget napomene

Prijava buga koja je dovela ovdje je vrsta koju u prvi mah ne vjerujete. Klijent spljošti potpisani obrazac suglasnosti, otvori rezultat, i svaki checkbox je prazan. Otvorite izvornu datoteku u Acrobatu i kućice su vidljivo označene. Pročitajte izvornu datoteku natrag kroz istu komponentu i vrijednosti polja su ispravne. Samo spljošteni izlaz ih gubi, i samo za checkboxeve i radio gumbe: tekstualna polja na istoj stranici ispadnu ispravno

Zašto su checkboxevi neoznačeni nakon spljoštenja?

Zato što spljoštenje nikad ne pogleda /V. FPDFPage_Flatten peče tok izgleda widgeta u sadržaj stranice, a izgled koji bira je onaj imenovan s /AS. Ako /AS i dalje kaže /Off dok vrijednost polja kaže da je kućica uključena, spljoštenje vjerno peče isključeni izgled. Vrijednost nikad nije bila izgubljena; nikad nije bila konzultirana

ISO 32000-1 §12.5.5 definira rječnik izgleda /AP s tri moguća unosa, /N, /R i /D. Za checkbox ili radio gumb unos /N nije tok, nego podrječnik čiji su ključevi imena stanja izgleda, a §12.5.2 čini /AS obaveznim selektorom kad je /N podrječnik. Dakle checkbox nosi dva unaprijed izgrađena izgleda i jedan pokazivač. Pogriješite pokazivač i renderiranje je pogrešno na način koji nikakva ispravna /V vrijednost neće popraviti. Ovo je i razlog zašto se način neuspjeha razlikuje od tekstualnih polja, koja nemaju unaprijed izgrađen izgled za odabrati uopće: tekstualno polje /N je jedan tok koji se mora regenerirati od nule nakon promjene vrijednosti, pa GenerateFormAppearances obrađuje ta dva slučaja kroz potpuno odvojene putove kôda, i samo je put gumba bio pokvaren

Gdje zapravo živi vrijednost checkboxa?

Na rječniku polja, ne na widgetu. ISO 32000-1 §12.7.5.2 opisuje checkboxeve i radio gumbe kao polja gumba čiji je /V objekt imena koji imenuje trenutno stanje izgleda, a §12.7.3.1 stavlja /V među unose zajedničke svim rječnicima polja. Widget napomena definirana u §12.5.6.19 pridonosi /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 točno ono što mu ime kaže: dohvati string unos iz rječnika napomene koji ste mu predali. Traženje /V na objektu 13 vraća ništa jer objekt 13 doista nema /V. Kvar je bio u pozivatelju, koji je pretpostavio ravan model objekta koji ISO 32000-1 nikad nije obećao

Kad polje i widget dijele jedan rječnik?

Kad god polje ima točno jedan widget. §12.5.6.19 dopušta da se rječnik polja i njegova jedina widget napomena spoje u jedan objekt, i većina alata za izradu koristi tu prečicu. U spojenom objektu /FT, /T, /V, /AS i /AP svi sjede jedan pored drugog, pa čitanje /V na razini widgeta uspijeva, i cijeli bug ostaje nevidljiv

U trenutku kad polje posjeduje dva ili više widgeta, spajanje je nemoguće, i §12.7.3.1 zahtijeva da widgeti postanu /Kids zasebnog rječnika polja. Svaka radio grupa je u ovom obliku po konstrukciji. Isto tako su i checkboxevi suglasnosti ponovljeni u zaglavlju i podnožju, i bilo koje polje koje je alat za izradu kopirao na drugu stranicu. To je cijelo objašnjenje zašto je kvar preživio regresijski paket: testni korpus bio je pun obrazaca s jednim widgetom, a klijentove datoteke nisu bile. Ako sami hodate widgetima umjesto da se oslanjate na komponentu, ista asimetrija pojavljuje se u redoslijedu nabrajanja, a bilješke o navigaciji poljima PDF obrazaca s PDFium Component pokrivaju kako se šetnja napomenama na razini stranice odnosi na stablo polja na razini dokumenta

Čitanje vrijednosti na način koji PDFium namjerava

FPDFAnnot_GetFormFieldValue je ispravan API, i bio je povezan u komponenti neko vrijeme prije nego što ga je put checkboxa počeo koristiti. Uzima ručku obrasca uz napomenu, što je signal koji je bitan: s dostupnim okruženjem popunjavanja obrazaca, PDFium razrješava napomenu u njezinu kontrolu obrasca i čita vrijednost s objekta polja, pa vraća ispravan odgovor i za spojene i za razdvojene rasporede

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;

Dvije pojedinosti u tom isječku lako je pogrešno shvatiti. Vraćena duljina je broj bajtova za UTF-16 tekst uključujući terminator, pa je broj znakova buflen div 2 - 1, a vrijednost 2 znači prazan niz. Zaštita buflen >= 4 stoga znači barem jedan pravi znak, što je ono što sprječava da polje bez ikakvog /V dobije prepisan svoj /AS praznim imenom

Oko čega se /AS i /AP /N zapravo slažu?

Slažu se oko imena, a ime bira tko god je proizveo datoteku. §12.7.5.2 zahtijeva da se isključeno stanje zove /Off, a uključeno stanje potpuno prepušta proizvođaču. /Yes je konvencija, ne pravilo. Acrobat piše /Yes, ali mnogi generatori pišu /On, /1, /Choice1, ili lokaliziranu riječ, a radio grupa obično daje svakom djetetu drukčije ime uključenog stanja tako da grupa može izraziti koji je gumb odabran. To je upravo razlog zašto je kopiranje /V doslovno u /AS ispravna operacija, a ne trik: za označenu kontrolu PDFium prijavljuje ime uključenog stanja koje sama datoteka definira, a za neoznačenu prijavljuje Off, pa je vrijednost koju upisujete u /AS zajamčeno ključ koji postoji u podrječniku /AP /N tog widgeta. Tvrdo kodiranje /Yes radilo bi na Acrobatovom izlazu, a tiho bi se pokvarilo svugdje drugdje

Redoslijed operacija, i gdje je i dalje potreban oprez

Redoslijed je fiksan i neopraštajući: omogući popunjavanje obrazaca, dodijeli vrijednosti, regeneriraj izglede, spljošti, zatim spremi. Preskočite korak regeneracije i FPDFPage_Flatten pronađe prazne ili zastarjele tokove izgleda i peče ih bez prigovora, što je tihi gubitak podataka, a ne povratna pogreš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');

Dva iskrena ograničenja ostaju. Prvo, sinkronizacija upisuje vrijednost polja u /AS svakog widgeta tog polja, što je ispravno za checkboxeve, a približno za radio grupe čije djeca svako definira vlastito ime uključenog stanja; dijete čiji /AP /N nema unos koji se podudara s upisanim /AS nema izgled za odabrati prema §12.5.5, pa neoznačeni gumb može spljoštiti u ništa umjesto u prazan krug. Revizija radio grupe s FPDFAnnot_GetFormControlIndex prije spljoštenja vrijedi tih nekoliko redaka. Drugo, ništa od ovoga se ne primjenjuje na XFA, gdje vrijednost živi u XML paketu podataka umjesto u AcroForm rječnicima, razdvajanje pokriveno u bilješkama o izmjenama XFA polja koje se ne trajno pohranjuju. Opća pouka vrijedi zadržati izvan ovog popravka: kad god API uzima ručku obrasca uz napomenu, govori vam da će razriješiti hijerarhiju polja umjesto vas, a kad god uzima samo napomenu, čitat će točno objekt koji ste proslijedili. Ta razlika također upravlja razmjenom podataka, budući da izvoz i uvoz XFDF podataka obrazaca radi u potpuno kvalificiranim imenima polja, nikad u pozicijama widgeta

Spljoštenje obrazaca jedna je od onih značajki koja izgleda kao jedan API poziv, a ispadne ugovor između tri rječnika. Ako biste radije radili s komponentom koja već kodira taj ugovor, PDFium Component za Delphi i C++Builder isporučuje regeneraciju izgleda, spljoštenje i pristup poljima obrazaca opisan ovdje kao obična svojstva i metode