Teknisk artikkel

Widget-indeks vs annotasjonsindeks i PDFium Delphi-skjemaer

I PDFium Component, den PDFium-baserte VCL/LCL-komponenten for Delphi, C++Builder, og Lazarus, er en skjemafelt-indeks ikke en annotasjonsindeks. En side bærer Link-, Text-, og Ink-annotasjoner ved siden av widgetene sine, så feltopplisting må filtrere på FPDFAnnot_GetSubtype og eksponere en null-basert logisk indeks, mappet tilbake til en ekte annotasjonsposisjon bare ved det native kallet

Feilen som avdekker dette er utvetydig når du først har sett den. En tester trykker Tab i et utfylt faktura-skjema og markøren forsvinner, fordi fokus gikk til en hyperlenke i bunnteksten. Eller verre, ingenting skjer i det hele tatt: koden din registrerer felt 3 som fokusert, UI-panelet oppdateres, og FORM_SetFocusedAnnot returnerte stille false hele tiden. Begge symptomene kommer fra samme designfeil, og én av dem har en andre rotårsak som gjemmer seg under

De to indeksrommene PDFium gir deg

PDFium eksponerer to nummereringsskjemaer over samme side, og de sammenfaller bare på dokumenter som tilfeldigvis ikke inneholder noe annet enn skjema-widgets. Det første er annotasjonsindeksen: en posisjon i side-/Annots-arrayen, som er det FPDFPage_GetAnnotCount teller og det FPDFPage_GetAnnot tar (ISO 32000-1 §12.5.2). Det andre er den logiske feltindeksen en applikasjonsnivå-API bør tilby, som løper fra null over de interaktive feltene en bruker faktisk kan nå. ISO 32000-1 §12.5.6.19 definerer widget-annotasjoner som den visuelle representasjonen av interaktive skjemafelt, og §12.7 definerer selve skjemaet. Alt annet på siden er en annen undertype med annen semantikk: en Link-annotasjon har en destinasjon, en Ink-annotasjon har en strøkliste, en Text-annotasjon er en gul lapp. Ingen av dem hører hjemme i et felttall, og ingen av dem kan motta skjemafokus. Likevel sitter de i /Annots-arrayen sammenflettet med widgetene i hvilken som helst rekkefølge det produserende programmet skrev dem, som ofte ikke er rekkefølgen noe annet ved dokumentet antyder

Hvorfor lander Tab på en hyperlenke i stedet for neste felt?

Fordi felttallet egentlig var et annotasjonstall. Den originale implementasjonen returnerte FPDFPage_GetAnnotCount direkte fra FormFieldCount, mens feltinformasjon-aksessoren, tab-rekkefølge-hjelperen, og fokus-hjelperen alle behandlet det samme heltallet som en widget-posisjon. På en ren AcroForm-side med seks widgets og ingenting annet er seks lik seks og hver test består. Legg til en hyperlenke i bunnteksten og en reviewer-kommentar i margen, og tallet rapporterer åtte felt, indeks 6 og 7 løser seg opp til ikke-skjema-objekter, og Tab går rett inn i dem

Fiksen ved opplistingsenden er å telle undertyper fremfor annotasjoner. Åpne hver annotasjon, spør om undertypen dens, behold widgetene, og lukk håndtaket i en finally-blokk, fordi FPDFPage_GetAnnot returnerer et eid håndtak som må gå tilbake gjennom 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;

Legg merke til hva dette bevisst ikke gjør. Det spør ikke skjema-fyll-miljøet om noe, og det trenger ikke et skjemahåndtak, fordi undertypen bor i annotasjonsordboken og er lesbar fra siden alene. Det betyr noe for rekkefølge: tallet er tilgjengelig før du har bestemt om dokumentet i det hele tatt fortjener et skjema-fyll-miljø, som artikkelen om AcroForm JavaScript og vert-hendelser dekker som en sikkerhetsbeslutning fremfor en bekvemmelighetsbeslutning

Å mappe den logiske indeksen tilbake ved den native grensen

Regelen som holder de to rommene fra å lekke inn i hverandre er enkel: den logiske indeksen er det eneste tallet som krysser din offentlige API, og den konverteres til en annotasjonsindeks i den siste funksjonen før det native kallet. Én mapping-hjelper, brukt av feltinfo, fokus, flagg-settere, og tab-rekkefølge likt, er det som gjør den regelen håndhevbar

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 egenskaper ved denne hjelperen er verdt å si rett ut. Det er en lineær skanning, så en naiv løkke over hvert felt koster et kvadratisk antall annotasjonsåpninger på en side med hundrevis av widgets; hvis du lister opp hele siden, gå gjennom annotasjonene én gang og samle inn widget-håndtakene mens du går fremfor å kalle mapperen per felt. Og den returnerer -1 fremfor å kaste et unntak, som lar kalleren avgjøre om en foreldet indeks er en programmeringsfeil verdt et unntak eller et kappløp verdt å ignorere, for eksempel etter at en redigering fjernet en annotasjon en mellomlagret UI-liste fortsatt refererer til

Hvorfor feiler FORM_SetFocusedAnnot på en headless side?

Fordi PDFium nekter å fokusere en widget hvis sidevisning aldri ble markert gyldig. FORM_SetFocusedAnnot løser opp annotasjonen til en sidevisning inne i skjema-fyll-miljøet, og hvis den sidevisningen ikke finnes returnerer den false uten noen diagnose. Å rette bare indeks-mappingen fikser derfor Tab som lander på en hyperlenke men lar det andre symptomet stå urørt: det logiske fokusregisteret ditt sier felt 3, den native fokuserte widgeten er fortsatt ingenting, og hver aksessor bygget på det native fokuset, fokusert tekst, fokusert verdi, valg-utvalgstilstand, fortsetter å returnere tomt. Sidevisningen opprettes av FORM_OnAfterLoadPage og ødelegges av FORM_OnBeforeClosePage. I en fremviser bygget rundt en visuell kontroll skjer de kallene som en del av å vise en side, som er hvorfor feilen så ofte ser ut som en bare-headless-feil: samme kode som fungerer i GUI-demoen feiler i batch-verktøyet. Livssyklusen tilhører dokumentobjektet, ikke fremviseren, så PDFium Component utsteder nå begge kallene når som helst en side lastes eller avlastes med et skjemahåndtak til stede. C-signaturen tar siden først og skjemahåndtaket andre, som er lett å invertere når man skriver bindingen for hånd

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;

Sjekken som beviser fiksen er den som sammenligner de to sidene. Kall FocusFormField med en logisk indeks, les deretter en verdi gjennom en aksessor som går gjennom den native fokuserte widgeten fremfor gjennom ditt eget register, som FocusedFormFieldValue eller FocusedFormOptionSelected. Hvis den logiske indeksen ruter rundtur men den native aksessoren kommer tilbake tom, er det sidevisningen som mangler, ikke mappingen

Hva den logiske feltindeksen ikke lover

En null-basert feltindeks er en bekvemmelighet, ikke en semantisk identitet, og fire grenser følger av det. Den er per side, ikke per dokument, så indeks 0 på side 2 er en annen widget enn indeks 0 på side 1 og å sammenligne dem er meningsløst. Den er posisjonell, så å sette inn eller slette en annotasjon ugyldiggjør hver mellomlagret indeks over endringen; behandle en lagret indeks som gyldig bare så lenge siden forblir lastet og uredigert

Den tredje grensen er den som overrasker folk som gjennomgår en feltliste. Indeksen lister opp widgets, ikke felt. En radiogruppe er ett felt med flere widget-kids, så en tre-knapps-gruppe bidrar med tre etterfølgende indekser som alle rapporterer samme Name. TPdfFormFieldInfo-recorden bærer GroupCount og GroupIndex for akkurat dette tilfellet, og en liste-UI som ignorerer dem viser samme felt tre ganger. Den fjerde grensen gjelder gjennomgangsrekkefølge: tab-rekkefølgen eksponert her er widget-opplistingsrekkefølgen, som følger /Annots-arrayen, ikke side-/Tabs-oppføringen (ISO 32000-1 §7.7.3.3) og ikke AcroForm-felttreet. For de fleste produsenter stemmer de overens; for et skjema lagt opp i to kolonner av en generator som utstedte høyre kolonne først, gjør de ikke det, og tastatur-veien beskrevet i artikkelen om skjemafeltnavigasjon vil føles feil selv om hver indeks er korrekt. Når en kundefil oppfører seg merkelig, dump begge indeksrommene side om side før du teoretiserer: annotasjonsvisningen og feltvisningen av samme side, skrevet ut sammen, gjør vanligvis årsaken åpenbar med ett blikk

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;

Et annotasjonstall langt over felttallet betyr at siden blander undertyper, som er normalt i gjennomgåtte dokumenter og er akkurat situasjonen mappingen finnes for; artikkelen om annotasjon-gjennomgang-arbeidsflyt ser på samme side fra markup-siden. Like tall på hver testfil betyr derimot at fixturene dine ikke kan oppdage denne feilklassen i det hele tatt, og det ærlige svaret er å legge til en skjema-fixture som bærer en lenke og en gul lapp

Feltopplistingen, fokuset, og annotasjon-API-ene beskrevet her leveres med PDFium Component for Delphi, C++Builder, og Lazarus, hvis produktside inneholder den fullstendige skjemafelt-referansen inkludert feltinformasjon-recorden og fokus-aksessorene