Odborný článok

Widget Index vs Annotation Index v PDFium Delphi formulároch

V PDFium Component, komponente založenej na PDFium pre VCL/LCL pre Delphi, C++Builder a Lazarus, nie je index poľa formulára indexom anotácie. Stránka nesie anotácie Link, Text a Ink vedľa svojich widgetov, takže enumerácia polí musí filtrovať podľa FPDFAnnot_GetSubtype a vystaviť nula-based logický index, mapovaný späť na skutočnú pozíciu anotácie len pri natívnom volaní

Bug, ktorý toto odhaľuje, je nezameniteľný, len čo ste ho raz videli. Tester stlačí Tab vo vyplnenej faktúre formulára a kurzor zmizne, pretože focus išiel na hyperlink v pätičke. Alebo horšie, nič sa nestane vôbec: váš kód zaznamená pole 3 ako fokusnuté, UI panel sa aktualizuje, a FORM_SetFocusedAnnot potichu vrátilo false celý čas. Oba symptómy pochádzajú z tej istej dizajnovej chyby, a jeden z nich má druhú príčinu skrytú pod ňou

Dva priestory indexov, ktoré vám PDFium dáva

PDFium vystavuje dve číslovacie schémy nad tou istou stránkou, a zhodujú sa len na dokumentoch, ktoré náhodou neobsahujú nič okrem form widgetov. Prvý je index anotácie: pozícia v poli stránky /Annots, čo je to, čo počíta FPDFPage_GetAnnotCount a čo berie FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Druhý je logický index poľa, ktorý by malo API na úrovni aplikácie ponúkať, bežiaci od nuly cez interaktívne polia, ku ktorým sa používateľ skutočne dostane. ISO 32000-1 §12.5.6.19 definuje widget anotácie ako vizuálnu reprezentáciu interaktívnych polí formulára, a §12.7 definuje samotný formulár. Všetko ostatné na stránke je iný podtyp s inou sémantikou: Link anotácia má destináciu, Ink anotácia má zoznam ťahov, Text anotácia je lepiaca poznámka. Žiadna z nich nepatrí do počtu polí, a žiadna z nich nemôže prijať focus formulára. Napriek tomu v poli /Annots sedia prekladané s widgetmi v akomkoľvek poradí, v akom ich producujúca aplikácia zapísala, čo je často nie poradie, ktoré by naznačovalo čokoľvek iné v dokumente

Prečo Tab pristane na hyperlinku namiesto ďalšieho poľa?

Pretože počet polí bol v skutočnosti počet anotácií. Pôvodná implementácia vracala FPDFPage_GetAnnotCount priamo z FormFieldCount, kým prístupový objekt informácií o poli, pomocník poradia tabulátora a pomocník focusu všetky traktovali to isté celé číslo ako pozíciu widgetu. Na čistej AcroForm stránke so šiestimi widgetmi a ničím iným sa šesť rovná šesť a každý test prejde. Pridajte hyperlink v pätičke a poznámku recenzenta na okraji, a počet nahlási osem polí, indexy 6 a 7 sa vyriešia na non-form objekty, a Tab vojde rovno do nich

Oprava na konci enumerácie je počítať podtypy namiesto anotácií. Otvorte každú anotáciu, opýtajte sa na jej podtyp, ponechajte widgety, a zatvorte handle v bloku finally, pretože FPDFPage_GetAnnot vráti vlastnený handle, ktorý sa musí vrátiť späť cez FPDFPage_CloseAnnot

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;

Všimnite si, čo toto zámerne nerobí. Nič sa nepýta form-fill prostredia, a nepotrebuje form handle, pretože podtyp žije v slovníku anotácie a je čitateľný len zo stránky. Na tom záleží pri usporiadaní: počet je dostupný skôr, než ste sa rozhodli, či si dokument vôbec zaslúži form-fill prostredie, čo článok o AcroForm JavaScript a host udalostiach pokrýva ako bezpečnostné rozhodnutie, nie pohodlie

Mapovanie logického indexu späť na natívnej hranici

Pravidlo, ktoré zabraňuje týmto dvom priestorom unikať jeden do druhého, je jednoduché: logický index je jediné číslo, ktoré prekračuje vaše verejné API, a konvertuje sa na index anotácie v poslednej funkcii pred natívnym volaním. Jeden pomocník mapovania, používaný informáciami o poli, focusom, nastavovačmi príznakov, a poradím tabulátora rovnako, je to, čo robí toto pravidlo vynútiteľným

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;

Stoja za priame vyslovenie dve vlastnosti tohto pomocníka. Je to lineárny scan, takže naivná slučka cez každé pole stojí kvadratický počet otvorení anotácie na stránke so stovkami widgetov; ak enumerujete celú stránku, prejdite anotácie raz a zbierajte handly widgetov postupne namiesto volania mapovača na pole. A vráti -1 namiesto vyvolania výnimky, čo nechá na volajúcom rozhodnúť, či je zastaraný index programátorská chyba hodná výnimky, alebo pretek hodný ignorovania, napríklad po tom, čo úprava odstránila anotáciu, na ktorú cachovaný UI zoznam stále odkazuje

Prečo FORM_SetFocusedAnnot zlyhá na headless stránke?

Pretože PDFium odmietne fokusovať widget, ktorého page view nebolo nikdy označené ako platné. FORM_SetFocusedAnnot vyrieši anotáciu na page view vnútri form-fill prostredia, a ak tento page view neexistuje, vráti false bez akejkoľvek diagnostiky. Oprava mapovania indexu sama teda opraví Tab pristávajúci na hyperlinku, ale ponechá druhý symptóm nedotknutý: váš záznam logického focusu hovorí pole 3, natívny fokusnutý widget je stále nič, a každý accessor postavený na natívnom focuse, fokusnutý text, fokusnutá hodnota, stav výberu voľby, sa naďalej vracia prázdny. Page view sa vytvára cez FORM_OnAfterLoadPage a ničí cez FORM_OnBeforeClosePage. Vo vieweri postavenom okolo vizuálneho controlu sa tieto volania dejú ako súčasť zobrazenia stránky, čo je dôvod, prečo bug tak často vyzerá ako headless-only: ten istý kód, ktorý funguje v GUI demo, zlyhá v batch nástroji. Lifecycle patrí objektu dokumentu, nie vieweru, takže PDFium Component teraz vydáva oba volania vždy, keď sa stránka načíta alebo uvoľní s prítomným form handle. C signatúra berie stránku prvú a form handle druhý, čo sa dá ľahko obrátiť pri ručnom písaní bindingu

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;

Kontrola, ktorá dokazuje opravu, je tá, ktorá porovná obe strany. Zavolajte FocusFormField s logickým indexom, potom prečítajte hodnotu cez accessor, ktorý ide cez natívny fokusnutý widget namiesto cez váš vlastný záznam, ako FocusedFormFieldValue alebo FocusedFormOptionSelected. Ak sa logický index round-tripuje, ale natívny accessor sa vráti prázdny, chýba page view, nie mapovanie

Čo logický index poľa nesľubuje

Nula-based index poľa je pohodlie, nie sémantická identita, a z toho vyplývajú štyri limity. Je per stránka, nie per dokument, takže index 0 na stránke 2 je iný widget než index 0 na stránke 1, a ich porovnávanie je bezvýznamné. Je pozičný, takže vloženie alebo vymazanie anotácie znehodnotí každý cachovaný index nad zmenou; traktujte uložený index ako platný len tak dlho, kým stránka ostáva načítaná a needitovaná

Tretí limit je ten, ktorý prekvapí ľudí prezerajúcich zoznam polí. Index enumeruje widgety, nie polia. Radio skupina je jedno pole s niekoľkými widget kidmi, takže trojtlačidlová skupina prispieva tromi po sebe nasledujúcimi indexmi, ktoré všetky hlásia to isté Name. Záznam TPdfFormFieldInfo nesie GroupCount a GroupIndex presne pre tento prípad, a UI zoznam, ktorý ich ignoruje, ukazuje to isté pole trikrát. Štvrtý limit sa týka poradia prechodu: poradie tabulátora tu vystavené je poradie enumerácie widgetov, ktoré nasleduje pole /Annots, nie záznam stránky /Tabs (ISO 32000-1 §7.7.3.3) a nie strom polí AcroForm. Pre väčšinu producentov sa tieto zhodujú; pre formulár rozvrhnutý do dvoch stĺpcov generátorom, ktorý vydal pravý stĺpec ako prvý, sa nezhodujú, a cesta klávesnice popísaná v článku o navigácii polí formulára bude pôsobiť nesprávne, hoci je každý index správny. Keď sa súbor zákazníka správa čudne, vypíšte oba priestory indexov vedľa seba skôr, než teoretizujete: pohľad anotácie a pohľad poľa tej istej stránky, vytlačené spolu, zvyčajne odhalia príčinu na jeden pohľad

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;

Počet anotácií výrazne nad počtom polí znamená, že stránka mieša podtypy, čo je normálne v recenzovaných dokumentoch a je presne situácia, pre ktorú mapovanie existuje; článok o workflow revízie anotácií sa pozerá na tú istú stránku zo strany markupu. Rovnaké počty na každom testovacom súbore na druhej strane znamenajú, že vaše fixtures nedokážu túto triedu chýb vôbec detegovať, a poctivá reakcia je pridať fixture formulára, ktorý nesie link a lepiacu poznámku

Enumerácia polí, focus a API anotácií tu popísané sa dodávajú s PDFium Component pre Delphi, C++Builder a Lazarus, ktorej produktová stránka nesie kompletnú referenciu polí formulára vrátane záznamu informácií o poli a accessorov focusu