Tehnički članak

Widget indeks naspram indeksa anotacije u PDFium Delphi formularima

U PDFium Component, VCL/LCL komponenti baziranoj na PDFium-u za Delphi, C++Builder, i Lazarus, indeks form polja nije indeks anotacije. Stranica nosi Link, Text, i Ink anotacije pored svojih widgeta, pa nabrajanje polja mora filtrirati po FPDFAnnot_GetSubtype i izložiti logički indeks od nule, mapiran nazad na stvarnu poziciju anotacije samo na nativnom pozivu

Bag koji ovo otkriva je nepogrešiv čim ga jednom vidite. Tester pritisne Tab u popunjenom formularu fakture i kursor nestane, jer je fokus otišao na hiperlink u podnožju. Ili gore, ne desi se ništa: vaš kod beleži polje 3 kao fokusirano, UI panel se ažurira, a FORM_SetFocusedAnnot je čitavo vreme tiho vraćao false. Oba simptoma dolaze iz iste greške u dizajnu, a jedan od njih ima drugi koreni uzrok skriven ispod

Dva prostora indeksa koje vam PDFium daje

PDFium izlaže dve šeme numeracije preko iste stranice, i one se poklapaju samo na dokumentima koji slučajno ne sadrže ništa osim form widgeta. Prvi je indeks anotacije: pozicija u nizu stranice /Annots, što je ono što FPDFPage_GetAnnotCount broji i što FPDFPage_GetAnnot uzima (ISO 32000-1 §12.5.2). Drugi je logički indeks polja koji bi API na nivou aplikacije trebalo da ponudi, koji ide od nule preko interaktivnih polja koja korisnik zaista može dosegnuti. ISO 32000-1 §12.5.6.19 definiše widget anotacije kao vizuelnu reprezentaciju interaktivnih form polja, a §12.7 definiše sam formular. Sve ostalo na stranici je drugačiji podtip sa drugačijom semantikom: Link anotacija ima destinaciju, Ink anotacija ima listu poteza, Text anotacija je lepljiva beleška. Nijedna od njih ne pripada brojanju polja, i nijedna ne može prihvatiti fokus formulara. Ipak u nizu /Annots sede isprepletane sa widgetima kojim god redosledom ih je aplikacija koja ih je proizvela zapisala, što često nije redosled koji išta drugo u vezi dokumenta sugeriše

Zašto Tab sleti na hiperlink umesto na sledeće polje?

Zato što je broj polja zapravo bio broj anotacija. Originalna implementacija je vraćala FPDFPage_GetAnnotCount direktno iz FormFieldCount, dok su pristupnik informacija o polju, helper redosleda tabulacije, i helper fokusa svi tretirali taj isti ceo broj kao poziciju widgeta. Na čistoj AcroForm stranici sa šest widgeta i ničim drugim, šest je jednako šest i svaki test prolazi. Dodajte hiperlink u podnožju i komentar recenzenta u margini, i broj prijavljuje osam polja, indeksi 6 i 7 se razrešavaju u objekte koji nisu forma, i Tab hoda pravo u njih

Popravka na strani nabrajanja je da se broje podtipovi, ne anotacije. Otvorite svaku anotaciju, pitajte za njen podtip, zadržite widgete, i zatvorite handle u finally bloku, jer FPDFPage_GetAnnot vraća handle u vlasništvu koji mora otići nazad kroz 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;

Obratite pažnju šta ovo namerno ne radi. Ne pita ništa okruženje za popunjavanje formulara, i ne treba mu form handle, jer podtip živi u rečniku anotacije i čitljiv je iz same stranice. To je bitno za redosled: broj je dostupan pre nego što ste odlučili da li dokument uopšte zaslužuje okruženje za popunjavanje formulara, što članak o AcroForm JavaScript-u i host eventima pokriva kao odluku bezbednosti, ne pogodnosti

Mapiranje logičkog indeksa nazad na nativnoj granici

Pravilo koje sprečava ta dva prostora da se izmešaju je jednostavno: logički indeks je jedini broj koji prelazi vaš javni API, i konvertuje se u indeks anotacije u poslednjoj funkciji pre nativnog poziva. Jedan helper mapiranja, korišćen podjednako od strane informacija o polju, fokusa, setera zastavica, i redosleda tabulacije, je ono što to pravilo čini sprovodivim

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;

Dve osobine ovog helpera vrede jasnog navođenja. To je linearni sken, pa naivna petlja preko svakog polja košta kvadratan broj otvaranja anotacija na stranici sa stotinama widgeta; ako nabrajate celu stranicu, obiđite anotacije jednom i skupljajte handle-ove widgeta usput umesto da pozivate mapper po polju. I vraća -1 umesto da baci grešku, što pozivaocu dozvoljava da odluči da li je zastareo indeks programerska greška vredna izuzetka ili trka vredna ignorisanja, na primer posle izmene koja je uklonila anotaciju na koju keširana UI lista i dalje referiše

Zašto FORM_SetFocusedAnnot otkazuje na headless stranici?

Zato što PDFium odbija da fokusira widget čiji prikaz stranice nikad nije bio obeležen kao validan. FORM_SetFocusedAnnot razrešava anotaciju u prikaz stranice unutar okruženja za popunjavanje formulara, a ako taj prikaz stranice ne postoji vraća false bez ikakve dijagnostike. Ispravljanje samog mapiranja indeksa zato popravlja da Tab sleti na hiperlink, ali ostavlja drugi simptom netaknut: vaš zapis logičkog fokusa kaže polje 3, nativno fokusiran widget je i dalje ništa, i svaki pristupnik izgrađen na nativnom fokusu, fokusiranom tekstu, fokusiranoj vrednosti, stanju izbora opcije, nastavlja da vraća prazno. Prikaz stranice kreira FORM_OnAfterLoadPage, a uništava FORM_OnBeforeClosePage. U vieweru izgrađenom oko vizuelne kontrole ti pozivi se dešavaju kao deo prikazivanja stranice, zato greška tako često izgleda kao bag samo za headless slučaj: isti kod koji radi u GUI demou otkazuje u batch alatu. Životni ciklus pripada objektu dokumenta, ne vieweru, pa PDFium Component sada izdaje oba poziva kad god se stranica učita ili istovari sa prisutnim form handle-om. C potpis uzima prvo stranicu, a form handle drugo, što je lako obrnuti kada ručno pišete binding

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;

Provera koja dokazuje popravku je ona koja upoređuje dve strane. Pozovite FocusFormField sa logičkim indeksom, zatim pročitajte vrednost kroz pristupnik koji ide preko nativno fokusiranog widgeta umesto preko vašeg sopstvenog zapisa, poput FocusedFormFieldValue ili FocusedFormOptionSelected. Ako logički indeks prođe round-trip, ali se nativni pristupnik vrati prazan, nedostaje prikaz stranice, ne mapiranje

Šta logički indeks polja ne obećava

Indeks polja od nule je pogodnost, ne semantički identitet, i iz toga slede četiri ograničenja. Po stranici je, ne po dokumentu, pa je indeks 0 na stranici 2 drugačiji widget od indeksa 0 na stranici 1, i njihovo poređenje je besmisleno. Pozicioni je, pa ubacivanje ili brisanje anotacije poništava svaki keširan indeks iznad izmene; tretirajte uskladišten indeks kao validan samo dok stranica ostaje učitana i neizmenjena

Treće ograničenje je ono koje iznenadi ljude koji pregledaju listu polja. Indeks nabraja widgete, ne polja. Radio grupa je jedno polje sa nekoliko widget dece, pa grupa od tri dugmeta doprinosi tri uzastopna indeksa koji svi prijavljuju isti Name. Zapis TPdfFormFieldInfo nosi GroupCount i GroupIndex tačno za ovaj slučaj, a lista UI koja ih ignoriše prikaže isto polje tri puta. Četvrto ograničenje se tiče redosleda obilaska: redosled tabulacije izložen ovde je redosled nabrajanja widgeta, koji prati niz /Annots, ne stavku /Tabs stranice (ISO 32000-1 §7.7.3.3) i ne stablo polja AcroForm-a. Za većinu proizvođača se ti redosledi poklapaju; za formular raspoređen u dve kolone od strane generatora koji je prvo emitovao desnu kolonu, ne poklapaju se, i putanja tastature opisana u članku o navigaciji form polja će delovati pogrešno iako je svaki indeks ispravan. Kada se korisnički fajl ponaša čudno, izbacite oba prostora indeksa jedan pored drugog pre teoretisanja: prikaz anotacija i prikaz polja iste stranice, ispisani zajedno, obično čine uzrok očiglednim na jedan pogled

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;

Broj anotacija daleko iznad broja polja znači da stranica meša podtipove, što je normalno u recenziranim dokumentima i tačno je situacija za koju mapiranje postoji; članak o workflow-u recenzije anotacija posmatra istu stranicu sa strane markupa. Jednaki brojevi na svakom test fajlu, s druge strane, znače da vaši fixturi uopšte ne mogu detektovati ovu klasu baga, i iskren odgovor je da dodate fixture formulara koji nosi link i lepljivu belešku

Nabrajanje polja, fokus, i API-ji anotacija opisani ovde isporučuju se sa PDFium Component za Delphi, C++Builder, i Lazarus, čija proizvodna stranica nosi kompletnu referencu form polja uključujući zapis informacija o polju i pristupnike fokusa