Műszaki cikk

PDF checkbox laposítási hiba: mezőérték vs widget Delphiben

A checkboxok és rádiógombok bejelöletlenül laposodnak, mert a megjelenítési állapot /AS sosem volt szinkronizálva a mezőértékkel /V. A PDFium Component, a PDFium-alapú VCL és LCL komponens Delphihez, C++Builderhez és Lazarushoz, most ezt az értéket az FPDFAnnot_GetFormFieldValue-val olvassa, amely a szülő mezőszótárt oldja fel a widget-annotáció helyett

A hibajelentés, amely idáig vezetett, olyan fajta, amelyben elsőre nem bízol. Egy ügyfél laposít egy aláírt beleegyezési nyomtatványt, megnyitja az eredményt, és minden checkbox üres. Nyisd meg a forrásfájlt Acrobatban, és a dobozok láthatóan be vannak pipálva. Olvasd vissza a forrásfájlt ugyanazon a komponensen keresztül, és a mezőértékek helyesek. Csak a laposított kimenet veszíti el őket, és csak checkboxoknál és rádiógomboknál: az ugyanazon az oldalon lévő szövegmezők rendben jönnek ki

Miért lesznek a checkboxok bejelöletlenek laposítás után?

Mert a laposítás sosem néz a /V-re. Az FPDFPage_Flatten belesüti a widget megjelenítési folyamát az oldaltartalomba, és a megjelenítés, amit kiválaszt, az, amelyet a /AS nevez meg. Ha a /AS még mindig /Off-ot mond, miközben a mezőérték azt mondja, a doboz be van kapcsolva, a laposítás hűségesen besüti a kikapcsolt megjelenítést. Az érték sosem veszett el; sosem lett megkérdezve

Az ISO 32000-1 §12.5.5 definiálja a /AP megjelenítési szótárat három lehetséges bejegyzéssel, /N, /R és /D. Egy checkbox vagy rádiógomb esetén az /N bejegyzés nem stream, hanem egy alszótár, amelynek kulcsai megjelenítési-állapot nevek, és a §12.5.2 teszi a /AS-t a kötelező szelektorrá, amikor az /N alszótár. Így egy checkbox két előre-elkészített megjelenítést és egy mutatót hordoz. Rontsd el a mutatót, és a renderelés rossz lesz, semennyi helyes /V ezt nem fogja javítani. Ez az oka annak is, hogy a hibamód eltér a szövegmezőktől, amelyeknek egyáltalán nincs előre-elkészített megjelenítésük, amit kiválaszthatnának: egy szövegmező /N-je egyetlen stream, amelyet nulláról kell regenerálni, miután az érték megváltozik, így a GenerateFormAppearances teljesen külön kódútvonalakon kezeli a két esetet, és csak a gomb-útvonal volt elromolva

Hol él valójában a checkbox érték?

A mezőszótáron, nem a widgeten. Az ISO 32000-1 §12.7.5.2 a checkboxokat és rádiógombokat gombmezőkként írja le, amelyeknek /V-je egy név-objektum, amely megnevezi az aktuális megjelenítési állapotot, és a §12.7.3.1 a /V-t az összes mezőszótár közös bejegyzései közé helyezi. A §12.5.6.19-ben definiált widget-annotáció adja a /AS-t és a /AP-t. Semmi a specifikációban nem kötelez egy widgetet arra, hogy /V-t hordozzon

// 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 }

Az FPDFAnnot_GetStringValue nem hibás. A szerződése pontosan az, amit a neve mond: húzz ki egy sztring-bejegyzést az annotáció-szótárból, amit átadtál neki. Ha megkéred a /V-t a 13-as objektumon, semmit nem kapsz vissza, mert a 13-as objektumnak valóban nincs /V-je. A hiba a hívóban volt, amely egy lapos objektummodellt feltételezett, amit az ISO 32000-1 sosem ígért

Mikor osztozik egy mező és egy widget egyetlen szótáron?

Amikor egy mezőnek pontosan egy widgetje van. A §12.5.6.19 megengedi, hogy a mezőszótár és annak egyetlen widget-annotációja egyetlen objektummá legyen egyesítve, és a legtöbb szerzői eszköz él ezzel a rövidítéssel. Egy egyesített objektumban a /FT, /T, /V, /AS és /AP mind egymás mellett ülnek, így egy widget-szintű /V-olvasás sikeres, és az egész hiba láthatatlan marad

Abban a pillanatban, hogy egy mező kettő vagy több widgetet birtokol, az egyesítés lehetetlenné válik, és a §12.7.3.1 megköveteli, hogy a widgetek egy különálló mezőszótár /Kids-jévé váljanak. Minden rádiócsoport ebben az alakban van, konstrukció szerint. Ugyanígy a fejlécben és lábjegyzetben megismételt beleegyezési checkboxok, és bármely mező, amelyet egy szerzői eszköz egy második oldalra másolt. Ez a teljes magyarázat arra, miért élte túl a hiba egy regressziós csomagot: a tesztkorpusz tele volt egyetlen-widgetes űrlapokkal, és az ügyfél fájlok nem. Ha te magad jársz be widgeteket ahelyett, hogy a komponensre hagyatkoznál, ugyanez az aszimmetria megjelenik a felsorolási sorrendben is, és a a PDFium Component-tel végzett PDF űrlapmező-navigáció jegyzetei tárgyalják, hogyan viszonyul egy oldal-szintű annotáció-bejárás a dokumentum-szintű mezőfához

Az érték olvasása úgy, ahogy a PDFium szánta

Az FPDFAnnot_GetFormFieldValue a helyes API, és egy ideje már be volt kötve a komponensben, anélkül hogy a checkbox útvonal használta volna. Az űrlap-fogantyút is átveszi az annotáció mellett, ami a jel, amely számít: az űrlap-kitöltő környezet elérhetőségével a PDFium feloldja az annotációt a form-vezérlőjére, és a mezőobjektumból olvassa az értéket, így a helyes választ adja vissza egyesített és szétválasztott elrendezéseknél egyaránt

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;

Két részletet könnyű elrontani ebben a részletben. A visszaadott hossz egy bájtszám UTF-16 szöveghez, beleértve a záró jelet, így a karakterszám buflen div 2 - 1, és a 2-es érték egy üres sztringet jelent. A buflen >= 4 őrzés ezért legalább egy valódi karaktert jelent, ami az, ami megakadályozza, hogy egy /V nélküli mező /AS-e felülíródjon egy üres névvel

Miben egyezik meg valójában a /AS és a /AP /N?

Egy névben egyeznek meg, és a nevet az választja meg, aki előállította a fájlt. A §12.7.5.2 megköveteli, hogy a kikapcsolt állapot /Off legyen elnevezve, és a bekapcsolt állapotot teljesen az előállítóra bízza. A /Yes egy konvenció, nem szabály. Az Acrobat /Yes-t ír, de rengeteg generátor ír /On-t, /1-et, /Choice1-et vagy egy lokalizált szót, és egy rádiócsoport normál esetben minden gyerekének megkülönböztetett bekapcsolt-állapot nevet ad, hogy a csoport kifejezhesse, melyik gomb van kiválasztva. Ez pontosan azért, mert a /V szó szerinti másolása a /AS-be a helyes művelet, nem egy hack: egy bejelölt vezérlőnél a PDFium azt a bekapcsolt-állapot nevet jelenti, amelyet maga a fájl definiál, egy be nem jelöltnél pedig Off-ot jelent, így az érték, amit a /AS-be írsz, garantáltan olyan kulcs, amely létezik abban a widget /AP /N alszótárában. A /Yes hardkódolása működne az Acrobat kimenetén, és csendben elromlana mindenhol máshol

Műveleti sorrend, és hol kell még figyelni

A szekvencia rögzített és megbocsáthatatlan: engedélyezd az űrlap-kitöltést, rendelj hozzá értékeket, regeneráld a megjelenítéseket, laposíts, majd ments. Hagyd ki a regenerálási lépést, és az FPDFPage_Flatten üres vagy elavult megjelenítési folyamokat talál, és panasz nélkül besüti őket, ami csendes adatvesztés, nem hibavisszatérés

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');

Két őszinte korlát marad. Először, a szinkronizálás a mezőérték minden widgetjének /AS-jébe írja azt, ami helyes checkboxoknál, és közelítő rádiócsoportoknál, amelyeknek minden gyereke saját bekapcsolt-állapot nevet definiál; egy gyerek, amelynek /AP /N-jében nincs a beírt /AS-nek megfelelő bejegyzés, nem rendelkezik megjelenítéssel, amelyet kiválaszthatna a §12.5.5 alatt, így egy nem kiválasztott gomb semmivé laposodhat egy üres kör helyett. Egy rádiócsoport auditálása az FPDFAnnot_GetFormControlIndex-szel laposítás előtt megéri a néhány sort. Másodszor, ebből semmi nem vonatkozik az XFA-ra, ahol az érték egy XML adatcsomagban él az AcroForm szótárak helyett, ami elválasztás, amit a a nem-perzisztált XFA mezőszerkesztésekről szóló jegyzetek tárgyalnak. Az általános tanulság érdemes megtartani ezen a javításon túl is: valahányszor egy API az űrlap-fogantyút is átveszi az annotáció mellett, azt mondja neked, hogy fel fogja oldani a mezőhierarchiát helyetted, és valahányszor csak az annotációt veszi át, pontosan azt az objektumot fogja olvasni, amit átadtál. Ez a megkülönböztetés az adatcserét is irányítja, mivel az XFDF űrlapadatok exportálása és importálása teljesen minősített mezőnevekben dolgozik, sosem widget-pozíciókban

Az űrlaplaposítás egyike azoknak a funkcióknak, amelyek egyetlen API-hívásnak látszanak, és három szótár közötti szerződésnek bizonyulnak. Ha inkább egy olyan komponenssel dolgoznál, amely már kódolja ezt a szerződést, a PDFium Component Delphihez és C++Builderhez hétköznapi tulajdonságokként és metódusokként szállítja az itt leírt megjelenítés-regenerálást, laposítást és űrlapmező-hozzáférést