Tehnični članak

Indeks widgeta proti indeksu opombe v obrazcih PDFium Delphi

V PDFium Component, komponenti VCL/LCL, zgrajeni na PDFium, za Delphi, C++Builder in Lazarus, indeks polja obrazca ni indeks opombe. Stran nosi opombe Link, Text in Ink zraven svojih widgetov, tako da mora naštevanje polj filtrirati po FPDFAnnot_GetSubtype in izpostaviti logičen indeks, oštevilčen od nič, preslikan nazaj na resničen položaj opombe le pri naravnem klicu

Hrošč, ki to razkrije, je nezamenljiv, ko ga enkrat vidite. Testiralec pritisne Tab v izpolnjenem obrazcu računa in kazalka izgine, ker je fokus šel na hiperpovezavo v nogi. Ali še huje, nič se ne zgodi: vaša koda zabeleži polje 3 kot fokusirano, plošča uporabniškega vmesnika se posodobi, FORM_SetFocusedAnnot pa je ves čas tiho vračal false. Oba simptoma izhajata iz iste napake zasnove, en od njiju pa ima drugi vzrok, skrit spodaj

Dva prostora indeksov, ki vam ju da PDFium

PDFium izpostavi dve shemi oštevilčenja nad isto stranjo, ujemata pa se le na dokumentih, ki slučajno ne vsebujejo ničesar razen widgetov obrazca. Prvi je indeks opombe: položaj v polju strani /Annots, kar šteje FPDFPage_GetAnnotCount in kar sprejme FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Drugi je logičen indeks polja, ki naj bi ga ponudil API na ravni aplikacije, in teče od nič čez interaktivna polja, ki jih lahko uporabnik dejansko doseže. ISO 32000-1 §12.5.6.19 opombe widgetov definira kot vizualno predstavitev interaktivnih polj obrazca, §12.7 pa definira sam obrazec. Vse drugo na strani je drugačen podtip z drugačno semantiko: opomba Link ima cilj, opomba Ink ima seznam potez, opomba Text je lepljiv listek. Nobena od njih ne sodi v štetje polj, nobena od njih pa ne more sprejeti fokusa obrazca. Kljub temu v polju /Annots sedijo prepleteni z widgeti v vrstnem redu, v katerem jih je zapisala izdelujoča aplikacija, kar pogosto ni vrstni red, ki bi ga nakazovalo karkoli drugega o dokumentu

Zakaj Tab pristane na hiperpovezavi namesto na naslednjem polju?

Ker je štetje polj v resnici bilo štetje opomb. Izvirna implementacija je vrnila FPDFPage_GetAnnotCount neposredno iz FormFieldCount, medtem ko so dostopnik informacij polja, pomočnik vrstnega reda tabulacije in pomočnik fokusa vsi to isto celo število obravnavali kot položaj widgeta. Na čisti strani AcroForm s šestimi widgeti in ničemer drugim šest je enako šest in vsak test prestane. Dodajte hiperpovezavo v nogi in komentar pregledovalca na robu, štetje pa poroča osem polj, indeksa 6 in 7 se razrešita v objekte, ki niso obrazci, Tab pa gre naravnost vanju

Popravek na strani naštevanja je šteti podtipe namesto opomb. Odprite vsako opombo, vprašajte za njen podtip, obdržite widgete in zaprite ročnik v bloku finally, ker FPDFPage_GetAnnot vrne lasten ročnik, ki se mora vrniti skozi 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;

Opazite, česa to namerno ne stori. Ničesar ne vpraša okolja izpolnjevanja obrazca in ne potrebuje ročnika obrazca, ker podtip živi v slovarju opombe in je berljiv že iz same strani. To je pomembno za vrstni red: štetje je na voljo, preden ste sploh odločili, ali si dokument zasluži okolje izpolnjevanja obrazca, kar članek o AcroForm JavaScript in gostiteljskih dogodkih obravnava kot varnostno odločitev, ne pa udobnostno

Preslikava logičnega indeksa nazaj na naravni meji

Pravilo, ki prostora drži narazen, je preprosto: logični indeks je edino število, ki prečka vaš javni API, v indeks opombe pa se pretvori v zadnji funkciji pred naravnim klicem. En pomočnik preslikave, ki ga uporabljajo informacije polja, fokus, nastavljalci zastavic in vrstni red tabulacije, je tisto, kar to pravilo naredi uveljivo

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;

Vredno je jasno navesti dve lastnosti tega pomočnika. Je linearno skeniranje, tako da naivna zanka čez vsako polje na strani s stotinami widgetov stane kvadratno število odpiranj opomb; če naštevate celo stran, prehodite opombe enkrat in med potjo zberite ročnike widgetov namesto klicanja preslikovalnika za vsako polje. In vrne -1 namesto da bi vrgel izjemo, kar klicatelju prepusti odločitev, ali je zastarel indeks programerska napaka, vredna izjeme, ali tekma, vredna ignoriranja, na primer po urejanju, ki je odstranilo opombo, na katero se predpomnjen seznam uporabniškega vmesnika še vedno sklicuje

Zakaj FORM_SetFocusedAnnot odpove na strani brez glave?

Ker PDFium zavrne fokusiranje widgeta, čigar pogled strani nikoli ni bil označen kot veljaven. FORM_SetFocusedAnnot opombo razreši na pogled strani znotraj okolja izpolnjevanja obrazca, če pa ta pogled strani ne obstaja, vrne false brez kakršne koli diagnostike. Sam popravek preslikave indeksa torej popravi Tab, ki pristane na hiperpovezavi, vendar pusti drugi simptom nedotaknjen: vaš logičen zapis fokusa pravi polje 3, naravno fokusiran widget pa je še vedno nič, vsak dostopnik, zgrajen na naravnem fokusu, fokusiranem besedilu, fokusirani vrednosti, stanju izbire izbire, pa še naprej vrača prazno. Pogled strani ustvari FORM_OnAfterLoadPage in uniči FORM_OnBeforeClosePage. V pregledovalniku, zgrajenem okoli vizualnega kontrolnika, se ta klica zgodita kot del prikazovanja strani, zato je odpoved tako pogosto videti kot hrošč, ki obstaja le brez glave: ista koda, ki deluje v demonstraciji GUI, odpove v paketnem orodju. Življenjski cikel pripada objektu dokumenta, ne pregledovalniku, tako da PDFium Component zdaj izda oba klica, kadarkoli je stran naložena ali razložena, medtem ko je prisoten ročnik obrazca. Podpis C sprejme najprej stran, nato ročnik obrazca, kar je enostavno obrniti pri ročnem pisanju vezave

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;

Preverjanje, ki dokaže popravek, je tisto, ki primerja obe strani. Pokličite FocusFormField z logičnim indeksom, nato pa preberite vrednost prek dostopnika, ki gre skozi naravno fokusiran widget namesto skozi vaš lasten zapis, kot je FocusedFormFieldValue ali FocusedFormOptionSelected. Če se logični indeks vrne pravilno, naravni dostopnik pa vrne prazno, pogled strani manjka, ne pa preslikava

Kaj logičen indeks polja ne obljublja

Indeks polja, oštevilčen od nič, je udobje, ne pa semantična identiteta, iz tega pa sledijo štiri meje. Je na stran, ne pa na dokument, tako da je indeks 0 na strani 2 drugačen widget od indeksa 0 na strani 1, njuna primerjava pa je nesmiselna. Je pozicijski, tako da vstavljanje ali brisanje opombe izniči vsak predpomnjen indeks nad spremembo; shranjen indeks obravnavajte kot veljaven le, dokler stran ostane naložena in neurejena

Tretja meja je tista, ki preseneti ljudi, ki pregledujejo seznam polj. Indeks naštevanja so widgeti, ne polja. Skupina izbirnih gumbov je eno polje z več widget-potomci, tako da tri-gumbna skupina prispeva tri zaporedne indekse, ki vsi poročajo isto Name. Zapis TPdfFormFieldInfo nosi GroupCount in GroupIndex natanko za ta primer, uporabniški vmesnik seznama, ki ju prezre, pa isto polje pokaže trikrat. Četrta meja zadeva vrstni red prehoda: vrstni red tabulacije, izpostavljen tukaj, je vrstni red naštevanja widgetov, ki sledi polju /Annots, ne pa vnosu strani /Tabs (ISO 32000-1 §7.7.3.3) in ne drevesu polj AcroForm. Za večino proizvajalcev se ta ujemata; za obrazec, postavljen v dva stolpca z generatorjem, ki je najprej izdal desni stolpec, se ne, pot s tipkovnico, opisana v članku o navigaciji polj obrazca, pa se bo počutila napačno, čeprav je vsak indeks pravilen. Kadar se datoteka stranke nenavadno obnaša, preden teoretizirate, izpišite oba prostora indeksov drug ob drugem: pogled opomb in pogled polj iste strani, natisnjena skupaj, ponavadi vzrok razjasnita v enem pogledu

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;

Štetje opomb, veliko nad štetjem polj, pomeni, da stran meša podtipe, kar je normalno v pregledanih dokumentih in je natanko situacija, za katero preslikava obstaja; članek o poteku pregleda opomb isto stran obravnava s strani označb. Enaki štetji na vsaki testni datoteki pa po drugi strani pomenita, da vaše pritrdilne naprave tega razreda hrošča sploh ne morejo zaznati, pošten odziv pa je dodati pritrdilno napravo obrazca, ki nosi povezavo in lepljiv listek

Naštevanje polj, fokus in API-ji opomb, opisani tukaj, so izdani z PDFium Component za Delphi, C++Builder in Lazarus, katerega stran izdelka nosi celoten referenčni opis polj obrazca, vključno z zapisom informacij polja in dostopniki fokusa