Teknisk artikel

Widget-indeks vs annotationsindeks i PDFium Delphi-formularer

I PDFium Component, den PDFium-baserede VCL/LCL-komponent til Delphi, C++Builder og Lazarus, er et formularfelt-indeks ikke et annotationsindeks. En side bærer Link-, Text- og Ink-annotationer ved siden af sine widgets, så feltopremsning skal filtrere på FPDFAnnot_GetSubtype og eksponere et nul-baseret logisk indeks, mappet tilbage til en reel annotationsposition kun ved det native kald

Fejlen, der afslører dette, er umiskendelig, når man først har set den. En tester trykker Tab i en udfyldt fakturaformular, og markøren forsvinder, fordi fokus gik til et hyperlink i sidefoden. Eller værre, intet sker overhovedet: din kode registrerer felt 3 som fokuseret, UI-panelet opdaterer, og FORM_SetFocusedAnnot returnerede stille falsk hele tiden. Begge symptomer kommer fra den samme designfejl, og ét af dem har en anden rodårsag gemt under

De to indeksrum PDFium giver dig

PDFium eksponerer to nummereringsskemaer over samme side, og de falder kun sammen på dokumenter, der tilfældigvis ikke indeholder andet end formularwidgets. Den første er annotationsindekset: en position i sidens /Annots-array, hvilket er hvad FPDFPage_GetAnnotCount tæller og hvad FPDFPage_GetAnnot tager (ISO 32000-1 §12.5.2). Den anden er det logiske feltindeks, en applikations-niveau-API bør tilbyde, løbende fra nul over de interaktive felter, en bruger faktisk kan nå. ISO 32000-1 §12.5.6.19 definerer widget-annotationer som den visuelle repræsentation af interaktive formularfelter, og §12.7 definerer selve formularen. Alt andet på siden er en anden subtype med anden semantik: en Link-annotation har en destination, en Ink-annotation har en strøg-liste, en Text-annotation er en klistermærke-note. Ingen af dem hører hjemme i en felttælling, og ingen af dem kan modtage formularfokus. Alligevel sidder de i /Annots-arrayet interfoldet med widgets i hvilken rækkefølge den producerende applikation end skrev dem, hvilket ofte ikke er den rækkefølge, alt andet ved dokumentet foreslår

Hvorfor lander Tab på et hyperlink i stedet for det næste felt?

Fordi felttællingen egentlig var en annotationstælling. Den oprindelige implementering returnerede FPDFPage_GetAnnotCount direkte fra FormFieldCount, mens felt-informationsaccessoren, tab-rækkefølge-hjælperen og fokus-hjælperen alle behandlede det samme heltal som en widget-position. På en ren AcroForm-side med seks widgets og intet andet er seks lig seks, og hver test består. Tilføj et hyperlink i sidefoden og en anmelder-kommentar i margenen, og tællingen rapporterer otte felter, indeks 6 og 7 opløser til ikke-formular-objekter, og Tab går lige ind i dem

Rettelsen i enden af opremsningen er at tælle subtyper frem for annotationer. Åbn hver annotation, spørg om dens subtype, behold widgets, og luk håndtaget i en finally-blok, fordi FPDFPage_GetAnnot returnerer et ejet håndtag, der skal gå tilbage gennem 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;

Bemærk hvad dette bevidst ikke gør. Det spørger ikke formular-udfyldningsmiljøet om noget, og det behøver ikke et formularhåndtag, fordi subtypen bor i annotations-dictionaryet og er læsbar fra siden alene. Det betyder noget for rækkefølge: tællingen er tilgængelig, før man har besluttet om dokumentet overhovedet fortjener et formular-udfyldningsmiljø, hvilket artiklen om AcroForm-JavaScript og host-hændelser dækker som en sikkerhedsbeslutning frem for en bekvemmelighedsbeslutning

At mappe det logiske indeks tilbage ved den native grænse

Reglen, der forhindrer de to rum i at lække ind i hinanden, er simpel: det logiske indeks er det eneste tal, der krydser din offentlige API, og det konverteres til et annotationsindeks i den sidste funktion før det native kald. Én mapnings-hjælper, brugt af felt-info, fokus, flag-sættere og tab-rækkefølge alike, er hvad der gør den regel håndhævelig

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;

To egenskaber ved denne hjælper er værd at nævne ligeud. Det er et lineært scan, så et naivt loop over hvert felt koster et kvadratisk antal annotationsåbninger på en side med hundredvis af widgets; hvis man opremser hele siden, gennemgå annotationerne én gang og saml widget-håndtagene undervejs i stedet for at kalde mapperen per felt. Og den returnerer -1 frem for at rejse en fejl, hvilket lader kalderen afgøre, om et forældet indeks er en programmeringsfejl værd en undtagelse eller et løb værd at ignorere, for eksempel efter en redigering fjernede en annotation, en cachet UI-liste stadig refererer til

Hvorfor fejler FORM_SetFocusedAnnot på en headless side?

Fordi PDFium nægter at fokusere en widget, hvis side-visning aldrig blev markeret gyldig. FORM_SetFocusedAnnot opløser annotationen til en side-visning inde i formular-udfyldningsmiljøet, og hvis den side-visning ikke findes, returnerer den falsk uden nogen diagnostik. At rette indeks-mapningen alene retter derfor Tab, der lander på et hyperlink, men lader det andet symptom være urørt: din logiske fokus-post siger felt 3, den native fokuserede widget er stadig intet, og hver accessor bygget på det native fokus, fokuseret tekst, fokuseret værdi, valg-tilstand, bliver ved med at returnere tomt. Side-visningen oprettes af FORM_OnAfterLoadPage og destrueres af FORM_OnBeforeClosePage. I en viewer bygget omkring en visuel kontrol sker de kald som del af at vise en side, hvilket er hvorfor fejlen så ofte ser ud som en kun-headless-fejl: den samme kode, der virker i GUI-demoen, fejler i batch-værktøjet. Livscyklussen hører til dokumentobjektet, ikke viewer'en, så PDFium Component udsteder nu begge kald, når en side indlæses eller udlæses med et formularhåndtag til stede. C-signaturen tager siden først og formularhåndtaget som nummer to, hvilket er let at vende om, når man skriver bindingen manuelt

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;

Tjekket, der beviser rettelsen, er det, der sammenligner de to sider. Kald FocusFormField med et logisk indeks, læs derefter en værdi gennem en accessor, der går gennem den native fokuserede widget frem for gennem din egen post, såsom FocusedFormFieldValue eller FocusedFormOptionSelected. Hvis det logiske indeks rundturer, men den native accessor kommer tomt tilbage, mangler side-visningen, ikke mapningen

Hvad det logiske feltindeks ikke lover

Et nul-baseret feltindeks er en bekvemmelighed, ikke en semantisk identitet, og fire begrænsninger følger af det. Det er per side, ikke per dokument, så indeks 0 på side 2 er en anden widget end indeks 0 på side 1, og at sammenligne dem er meningsløst. Det er positionelt, så at indsætte eller slette en annotation ugyldiggør hvert cachet indeks over ændringen; behandl et gemt indeks som gyldigt kun så længe siden forbliver indlæst og uredigeret

Den tredje begrænsning er den, der overrasker folk der gennemgår en feltliste. Indekset opremser widgets, ikke felter. En radioknap-gruppe er ét felt med flere widget-børn, så en tre-knappers gruppe bidrager tre fortløbende indekser, der alle rapporterer samme Name. TPdfFormFieldInfo-posten bærer GroupCount og GroupIndex netop til dette tilfælde, og en liste-UI, der ignorerer dem, viser det samme felt tre gange. Den fjerde begrænsning angår gennemløbsrækkefølge: tab-rækkefølgen eksponeret her er widget-opremsningsrækkefølgen, som følger /Annots-arrayet, ikke sidens /Tabs-post (ISO 32000-1 §7.7.3.3) og ikke AcroForm-felt-træet. For de fleste producenter er de enige; for en formular lagt ud i to kolonner af en generator, der udsendte den højre kolonne først, er de ikke, og tastatur-stien beskrevet i artiklen om formularfelt-navigation vil føles forkert, selvom hvert indeks er korrekt. Når en kundefil opfører sig underligt, dump begge indeksrum side om side, før man teoretiserer: annotationsvisningen og feltvisningen af samme side, printet sammen, gør som regel årsagen tydelig i ét blik

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;

En annotationstælling langt over felttællingen betyder, at siden blander subtyper, hvilket er normalt i gennemgåede dokumenter og er præcis den situation, mapningen findes for; artiklen om annotationsgennemgangs-workflow ser på samme side fra markup-siden. Lige tællinger på hver testfil, derimod, betyder at dine fixtures slet ikke kan detektere denne fejlklasse, og det ærlige svar er at tilføje en formular-fixture, der bærer et link og en klistermærke-note

Felt-opremsnings-, fokus- og annotations-API'erne beskrevet her leveres med PDFium Component til Delphi, C++Builder og Lazarus, hvis produktside bærer den fulde formularfelt-reference inklusive felt-informationsposten og fokus-accessorerne