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