Műszaki cikk

Widget-index vs annotáció-index PDFium Delphi űrlapokban

A PDFium Component-ben, a PDFium-alapú VCL/LCL komponensben Delphihez, C++Builderhez és Lazarushoz, egy űrlapmező-index nem annotáció-index. Egy oldal Link, Text és Ink annotációkat hordoz a widgetjei mellett, így a mező-felsorolásnak szűrnie kell az FPDFAnnot_GetSubtype-ra, és egy nulla-alapú logikai indexet kell kitennie, amelyet csak a natív hívásnál képeznek le vissza valódi annotáció-pozícióra

A hiba, amely ezt leleplezi, félreérthetetlen, ha egyszer láttad. Egy tesztelő megnyomja a Tab-ot egy kitöltött számla-űrlapon, és a kurzor eltűnik, mert a fókusz egy hiperhivatkozásra ment a lábjegyzetben. Vagy ami rosszabb, semmi sem történik: a kódod a 3-as mezőt fókuszáltként rögzíti, a UI panel frissül, és az FORM_SetFocusedAnnot csendben false-t adott vissza az egész idő alatt. Mindkét tünet ugyanabból a tervezési hibából jön, és az egyiknek van egy második gyökéroka is, amely alatta rejtőzik

A két indextér, amit a PDFium ad

A PDFium két számozási sémát tesz ki ugyanazon oldal fölött, és ezek csak olyan dokumentumokon esnek egybe, amelyek véletlenül semmi mást nem tartalmaznak, csak űrlap-widgeteket. Az első az annotáció-index: egy pozíció az oldal /Annots tömbjében, amit az FPDFPage_GetAnnotCount számol, és amit az FPDFPage_GetAnnot vesz át (ISO 32000-1 §12.5.2). A második a logikai mező-index, amelyet egy alkalmazás-szintű API-nak kellene kínálnia, nullától futva az interaktív mezők fölött, amelyeket egy felhasználó ténylegesen elérhet. Az ISO 32000-1 §12.5.6.19 a widget-annotációkat úgy definiálja, mint az interaktív űrlapmezők vizuális reprezentációját, és a §12.7 magát az űrlapot definiálja. Minden más az oldalon egy másik altípus, más szemantikával: egy Link annotációnak van célpontja, egy Ink annotációnak van vonás-listája, egy Text annotáció egy ragadós jegyzet. Egyik sem tartozik egy mezőszámba, és egyik sem fogadhat űrlap-fókuszt. Mégis a /Annots tömbben úgy ülnek a widgetek közé keverve, amilyen sorrendben az előállító alkalmazás írta őket, ami gyakran nem az a sorrend, amit a dokumentum bármi más része sugallna

Miért landol a Tab egy hiperhivatkozáson a következő mező helyett?

Mert a mezőszám valójában annotáció-szám volt. Az eredeti megvalósítás közvetlenül az FPDFPage_GetAnnotCount-ot adta vissza a FormFieldCount-ból, miközben a mező-információ-hozzáférő, a tab-sorrend segédfüggvény és a fókusz-segédfüggvény mind ugyanazt az egészt widget-pozícióként kezelte. Egy tiszta AcroForm oldalon hat widgettel és semmi mással, a hat egyenlő hattal, és minden teszt átmegy. Adj hozzá egy hiperhivatkozást a lábjegyzetben és egy reviewer-kommentet a margón, és a szám nyolc mezőt jelent, a 6-os és 7-es indexek nem-űrlap objektumokra oldódnak fel, és a Tab egyenesen beléjük sétál

A javítás a felsorolási végen az, hogy altípusokat számolunk, nem annotációkat. Nyiss meg minden annotációt, kérdezd meg az altípusát, tartsd meg a widgeteket, és zárd be a fogantyút egy finally blokkban, mert az FPDFPage_GetAnnot egy birtokolt fogantyút ad vissza, amelynek vissza kell mennie az FPDFPage_CloseAnnot-on keresztül

function WidgetCountForPage(Page: FPDF_PAGE): Integer;
var
  Count, I: Integer;
  Annot: FPDF_ANNOTATION;
begin
  Result := 0;
  if Page = nil then
    Exit;
  Count := FPDFPage_GetAnnotCount(Page);   // every annotation, not just fields
  for I := 0 to Count - 1 do
  begin
    Annot := FPDFPage_GetAnnot(Page, I);
    if Annot = nil then
      Continue;
    try
      if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
        Inc(Result);
    finally
      FPDFPage_CloseAnnot(Annot);
    end;
  end;
end;

Vedd figyelembe, mit nem tesz ez szándékosan. Nem kérdez semmit az űrlap-kitöltő környezettől, és nincs szüksége űrlap-fogantyúra, mert az altípus az annotáció-szótárban él, és olvasható az oldalról önmagában. Ez a sorrendnél számít: a szám elérhető, mielőtt eldöntötted volna, hogy a dokumentum egyáltalán megérdemel-e egy űrlap-kitöltő környezetet, amit a az AcroForm JavaScript és host-eseményekről szóló cikk biztonsági döntésként tárgyal, nem kényelmiként

A logikai index leképezése vissza a natív peremen

A szabály, amely megakadályozza, hogy a két tér egymásba szivárogjon, egyszerű: a logikai index az egyetlen szám, amely átkel a nyilvános API-don, és annotáció-indexre alakul az utolsó függvényben a natív hívás előtt. Egy leképező segédfüggvény, amelyet a mezőinfó, a fókusz, a jelző-beállítók és a tab-sorrend egyaránt használ, az, ami kikényszeríthetővé teszi ezt a szabályt

function AnnotationIndexForField(Page: FPDF_PAGE;
  FieldIndex: Integer): Integer;
var
  Count, I, Current: Integer;
  Annot: FPDF_ANNOTATION;
begin
  Result := -1;
  if (Page = nil) or (FieldIndex < 0) then
    Exit;
  Count := FPDFPage_GetAnnotCount(Page);
  Current := 0;
  for I := 0 to Count - 1 do
  begin
    Annot := FPDFPage_GetAnnot(Page, I);
    if Annot = nil then
      Continue;
    try
      if FPDFAnnot_GetSubtype(Annot) = FPDF_ANNOT_WIDGET then
      begin
        if Current = FieldIndex then
          Exit(I);        // real /Annots position: native calls only
        Inc(Current);
      end;
    finally
      FPDFPage_CloseAnnot(Annot);
    end;
  end;
end;

Két tulajdonsága ennek a segédfüggvénynek érdemes tisztán kimondani. Lineáris pásztázás, így egy naiv ciklus minden mező fölött kvadratikus számú annotáció-nyitást kerül egy több száz widgetes oldalon; ha az egész oldalt felsorolod, sétáld be az annotációkat egyszer, és gyűjtsd a widget-fogantyúkat menet közben ahelyett, hogy a leképezőt hívnád mezőnként. És -1-et ad vissza kivétel dobása helyett, ami engedi a hívónak eldönteni, hogy egy elavult index programozási hiba, amely megér egy kivételt, vagy egy verseny, amit érdemes figyelmen kívül hagyni, például miután egy szerkesztés eltávolított egy annotációt, amelyre egy gyorsítótárazott UI-lista még hivatkozik

Miért bukik el a FORM_SetFocusedAnnot egy headless oldalon?

Mert a PDFium megtagadja egy olyan widget fókuszálását, amelynek oldalnézete sosem lett érvényesnek jelölve. Az FORM_SetFocusedAnnot feloldja az annotációt egy oldalnézetre az űrlap-kitöltő környezeten belül, és ha az az oldalnézet nem létezik, false-t ad vissza bármilyen diagnózis nélkül. Az index-leképezés önmagában történő javítása ezért megjavítja a Tab hiperhivatkozásra landolását, de érintetlenül hagyja a második tünetet: a logikai fókusz-rekordod azt mondja, 3-as mező, a natív fókuszált widget még mindig semmi, és minden hozzáférő, amely a natív fókuszra épül, fókuszált szöveg, fókuszált érték, választás-kiválasztási állapot, továbbra is üresen tér vissza. Az oldalnézetet az FORM_OnAfterLoadPage hozza létre, és az FORM_OnBeforeClosePage pusztítja el. Egy vizuális vezérlő köré épített megjelenítőben ezek a hívások egy oldal megjelenítésének részeként történnek, ez az oka annak, hogy a hiba olyan gyakran néz ki headless-only hibának: ugyanaz a kód, amely működik a GUI demóban, elbukik a batch eszközben. Az életciklus a dokumentum objektumhoz tartozik, nem a megjelenítőhöz, így a PDFium Component most mindkét hívást kiadja, valahányszor egy oldalt betöltenek vagy kitöltenek egy űrlap-fogantyúval jelen. A C aláírás elsőnek az oldalt veszi át, másodiknak az űrlap-fogantyút, amit könnyű megfordítani a kötés kézzel írásakor

procedure ReportFirstField(const FileName: string);
var
  Pdf: TPdf;
  Idx: Integer;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FormFill := True;      // form-fill environment, before Active
    Pdf.FileName := FileName;
    Pdf.Active := True;
    Pdf.PageNumber := 1;       // page load also runs FORM_OnAfterLoadPage

    Idx := Pdf.FocusNextFormField;   // logical index, 0-based over widgets
    if Idx < 0 then
      Exit;                    // page holds no widget annotations

    Writeln(string(Pdf.FormFieldInfo[Idx].Name), ' = ',
      string(Pdf.FocusedFormFieldValue));   // reads the native focused widget
  finally
    Pdf.Free;                  // page unload runs FORM_OnBeforeClosePage
  end;
end;

Az ellenőrzés, amely a javítást bizonyítja, az, amelyik a két oldalt hasonlítja össze. Hívd a FocusFormField-et egy logikai indexszel, majd olvass egy értéket egy olyan hozzáférőn keresztül, amely a natív fókuszált widgeten keresztül megy, nem a saját rekordodon, mint a FocusedFormFieldValue vagy a FocusedFormOptionSelected. Ha a logikai index körbeér, de a natív hozzáférő üresen tér vissza, az oldalnézet hiányzik, nem a leképezés

Mit nem ígér a logikai mező-index

Egy nulla-alapú mező-index kényelem, nem szemantikai identitás, és négy korlát következik ebből. Oldalankénti, nem dokumentumonkénti, így a 0-s index a 2-es oldalon másik widget, mint a 0-s index az 1-es oldalon, és összehasonlításuk értelmetlen. Pozicionális, így egy annotáció beillesztése vagy törlése érvényteleníti minden gyorsítótárazott indexet a változás fölött; egy tárolt indexet csak addig kezelj érvényesként, amíg az oldal betöltve és szerkesztetlen marad

A harmadik korlát az, amely meglepi az embereket, akik egy mezőlistát vizsgálnak felül. Az index widgeteket sorol fel, nem mezőket. Egy rádiócsoport egyetlen mező több widget-gyerekkel, így egy három-gombos csoport három egymást követő indexet ad hozzá, amelyek mind ugyanazt a Name-et jelentik. A TPdfFormFieldInfo rekord hordozza a GroupCount-ot és GroupIndex-et pontosan erre az esetre, és egy lista-UI, amely figyelmen kívül hagyja őket, háromszor mutatja ugyanazt a mezőt. A negyedik korlát a bejárási sorrendet érinti: az itt kitett tab-sorrend a widget-felsorolási sorrend, amely a /Annots tömböt követi, nem az oldal /Tabs bejegyzését (ISO 32000-1 §7.7.3.3), és nem az AcroForm mezőfát. A legtöbb előállítónál ezek egyeznek; egy két oszlopba rendezett űrlapnál, amelyet egy generátor a jobb oszlopot elsőnek bocsátotta ki, nem egyeznek, és a az űrlapmező-navigáció cikkében leírt billentyűzet-útvonal rossznak fog érződni, még ha minden index helyes is. Amikor egy ügyfélfájl furcsán viselkedik, dobd ki mindkét indexteret egymás mellett, mielőtt elméleteznél: az annotáció-nézet és a mező-nézet ugyanarról az oldalról, együtt kinyomtatva, általában egy pillantásra nyilvánvalóvá teszi az okot

procedure DumpIndexSpaces(Pdf: TPdf);
var
  I: Integer;
  Info: TPdfFormFieldInfo;
begin
  for I := 0 to Pdf.AnnotationCount - 1 do
    Writeln('annot ', I, ': subtype ', Ord(Pdf.Annotation[I].Subtype));

  for I := 0 to Pdf.FormFieldCount - 1 do
  begin
    Info := Pdf.FormFieldInfo[I];
    Writeln('field ', I, ': ', string(Info.Name),
      ' widget ', Info.GroupIndex, ' of ', Info.GroupCount);
  end;
end;

Egy annotáció-szám, amely jóval a mezőszám fölött van, azt jelenti, hogy az oldal kever altípusokat, ami normális felülvizsgált dokumentumokban, és pontosan az a helyzet, amire a leképezés létezik; a az annotáció-felülvizsgálati munkafolyamat cikk ugyanazt az oldalt nézi a jelölés oldaláról. Egyenlő számok minden tesztfájlon, másrészt, azt jelentik, a fixtúráid egyáltalán nem tudják érzékelni ezt a hibaosztályt, és az őszinte válasz egy űrlap-fixtúra hozzáadása, amely egy linket és egy ragadós jegyzetet hordoz

Az itt leírt mező-felsorolási, fókusz- és annotáció-API-k a PDFium Component-tel érkeznek Delphihez, C++Builderhez és Lazarushoz, amelynek termékoldala tartalmazza a teljes űrlapmező-referenciát, beleértve a mezőinformáció-rekordot és a fókusz-hozzáférőket