In PDFium Component, de op PDFium gebaseerde VCL/LCL-component voor Delphi, C++Builder en Lazarus, is een formulierveldindex geen annotatie-index. Een pagina draagt Link-, Text- en Ink-annotaties naast zijn widgets, dus veldopsomming moet filteren op FPDFAnnot_GetSubtype en een nulgebaseerde logische index blootstellen, die pas bij de native aanroep wordt teruggekoppeld naar een echte annotatiepositie
De bug die dit blootlegt is onmiskenbaar zodra je hem hebt gezien. Een tester drukt op Tab in een ingevuld factuurformulier en de cursor verdwijnt, omdat de focus naar een hyperlink in de voettekst ging. Of erger, er gebeurt helemaal niets: je code registreert veld 3 als gefocust, het UI-paneel wordt bijgewerkt, en FORM_SetFocusedAnnot gaf de hele tijd stilzwijgend false terug. Beide symptomen komen voort uit dezelfde ontwerpfout, en één daarvan heeft een tweede onderliggende oorzaak
De twee indexruimtes die PDFium je aanreikt
PDFium stelt twee nummeringsschema's bloot over dezelfde pagina, en ze vallen alleen samen op documenten die toevallig niets anders dan formulierwidgets bevatten. De eerste is de annotatie-index: een positie in de pagina-array /Annots, wat FPDFPage_GetAnnotCount telt en wat FPDFPage_GetAnnot als parameter neemt (ISO 32000-1 §12.5.2). De tweede is de logische veldindex die een applicatieniveau-API zou moeten aanbieden, lopend vanaf nul over de interactieve velden die een gebruiker daadwerkelijk kan bereiken. ISO 32000-1 §12.5.6.19 definieert widget-annotaties als de visuele weergave van interactieve formuliervelden, en §12.7 definieert het formulier zelf. Al het andere op de pagina is een ander subtype met andere semantiek: een Link-annotatie heeft een bestemming, een Ink-annotatie heeft een lijst van penstreken, een Text-annotatie is een gele memoblaadje. Geen daarvan hoort in een veldtelling, en geen daarvan kan formulierfocus aannemen. Toch staan ze in de array /Annots verweven met de widgets, in welke volgorde de producerende applicatie ze ook heeft geschreven, wat vaak niet de volgorde is die de rest van het document zou suggereren
Waarom landt Tab op een hyperlink in plaats van op het volgende veld
Omdat de veldtelling eigenlijk een annotatietelling was. De oorspronkelijke implementatie gaf FPDFPage_GetAnnotCount rechtstreeks terug vanuit FormFieldCount, terwijl de veldinformatie-accessor, de tabvolgorde-helper en de focus-helper diezelfde integer allemaal als widgetpositie behandelden. Op een schone AcroForm-pagina met zes widgets en niets anders is zes gelijk aan zes en slaagt elke test. Voeg een hyperlink in de voettekst en een reviewercommentaar in de marge toe, en de telling rapporteert acht velden, indices 6 en 7 lossen op naar niet-formulierobjecten, en Tab loopt er recht in
De fix aan de opsommingskant is om subtypes te tellen in plaats van annotaties. Open elke annotatie, vraag naar het subtype, houd de widgets, en sluit de handle in een finally-blok, want FPDFPage_GetAnnot geeft een handle terug waarvan jij eigenaar bent en die terug moet via 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;
Merk op wat dit doelbewust niet doet. Het vraagt de form-fill-omgeving niets, en het heeft geen formulierhandle nodig, want het subtype leeft in het annotatiewoordenboek en is leesbaar vanuit de pagina alleen. Dat is van belang voor de volgorde: de telling is beschikbaar voordat je zelfs maar hebt beslist of het document überhaupt een form-fill-omgeving verdient, wat het artikel over AcroForm-JavaScript en host-events behandelt als een veiligheidsbeslissing eerder dan een gemakszaak
De logische index terugkoppelen aan de native grens
De regel die de twee ruimtes ervan weerhoudt in elkaar over te lopen is eenvoudig: de logische index is het enige getal dat je publieke API oversteekt, en het wordt omgezet naar een annotatie-index in de laatste functie vóór de native aanroep. Eén mapping-helper, gebruikt door veldinfo, focus, vlagsetters en tabvolgorde tegelijk, is wat die regel afdwingbaar maakt
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;
Twee eigenschappen van deze helper zijn het vermelden waard. Het is een lineaire scan, dus een naïeve lus over elk veld kost een kwadratisch aantal annotatie-opens op een pagina met honderden widgets; als je de hele pagina opsomt, doorloop dan de annotaties eenmaal en verzamel de widgethandles onderweg in plaats van de mapper per veld aan te roepen. En het geeft -1 terug in plaats van een exception op te werpen, wat de aanroeper laat beslissen of een verouderde index een programmeerfout is die een exception waard is, of een race die genegeerd mag worden, bijvoorbeeld nadat een bewerking een annotatie heeft verwijderd waar een gecachete UI-lijst nog naar verwijst
Waarom faalt FORM_SetFocusedAnnot op een headless pagina
Omdat PDFium weigert een widget te focussen wiens paginaweergave nooit als geldig was gemarkeerd. FORM_SetFocusedAnnot lost de annotatie op naar een paginaweergave binnen de form-fill-omgeving, en als die paginaweergave niet bestaat, geeft het false terug zonder enige diagnose. Het corrigeren van de indexmapping alleen lost dus wel Tab die op een hyperlink landt op, maar laat het tweede symptoom onaangeroerd: je logische focusrecord zegt veld 3, de native gefocuste widget is nog steeds niets, en elke accessor gebouwd op de native focus, gefocuste tekst, gefocuste waarde, keuzeselectiestatus, blijft leeg teruggeven. De paginaweergave wordt aangemaakt door FORM_OnAfterLoadPage en vernietigd door FORM_OnBeforeClosePage. In een viewer gebouwd rond een visueel besturingselement gebeuren die aanroepen als onderdeel van het weergeven van een pagina, wat is waarom de fout er zo vaak uitziet als een headless-only bug: dezelfde code die werkt in de GUI-demo faalt in de batchtool. De levenscyclus hoort bij het documentobject, niet bij de viewer, dus PDFium Component geeft nu beide aanroepen af telkens wanneer een pagina wordt geladen of ontladen met een formulierhandle aanwezig. De C-signatuur neemt eerst de pagina en dan de formulierhandle, wat makkelijk om te draaien is bij het handmatig schrijven van de 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;
De controle die de fix bewijst, is degene die de twee kanten vergelijkt. Roep FocusFormField aan met een logische index, en lees vervolgens een waarde via een accessor die via de native gefocuste widget gaat in plaats van via je eigen record, zoals FocusedFormFieldValue of FocusedFormOptionSelected. Als de logische index rondgaat maar de native accessor leeg terugkomt, ontbreekt de paginaweergave, niet de mapping
Wat de logische veldindex niet belooft
Een nulgebaseerde veldindex is een gemak, geen semantische identiteit, en daar volgen vier beperkingen uit. Ze is per pagina, niet per document, dus index 0 op pagina 2 is een andere widget dan index 0 op pagina 1 en ze vergelijken is zinloos. Ze is positioneel, dus het invoegen of verwijderen van een annotatie maakt elke gecachete index boven de wijziging ongeldig; behandel een opgeslagen index alleen als geldig zolang de pagina geladen en onbewerkt blijft
De derde beperking is degene die mensen verrast bij het beoordelen van een veldlijst. De index somt widgets op, geen velden. Een keuzerondjegroep is één veld met verscheidene widget-kinderen, dus een groep van drie knoppen levert drie opeenvolgende indices op die allemaal dezelfde Name rapporteren. Het record TPdfFormFieldInfo draagt GroupCount en GroupIndex precies voor dit geval, en een lijst-UI die deze negeert toont hetzelfde veld drie keer. De vierde beperking betreft doorloopvolgorde: de tabvolgorde die hier wordt blootgesteld is de widget-opsommingsvolgorde, die de array /Annots volgt, niet de ingang /Tabs van de pagina (ISO 32000-1 §7.7.3.3) en niet de AcroForm-veldboom. Voor de meeste producenten komen die overeen; voor een formulier ingedeeld in twee kolommen door een generator die eerst de rechterkolom uitzond, komen ze dat niet, en het toetsenbordpad beschreven in het artikel over formuliervveldnavigatie zal verkeerd aanvoelen ook al is elke index correct. Wanneer een klantbestand zich vreemd gedraagt, dump beide indexruimtes naast elkaar voordat je gaat theoretiseren: de annotatieweergave en de veldweergave van dezelfde pagina, samen afgedrukt, maken de oorzaak meestal in één oogopslag duidelijk
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;
Een annotatietelling die ver boven de veldtelling ligt betekent dat de pagina subtypes mengt, wat normaal is in beoordeelde documenten en precies de situatie waarvoor de mapping bestaat; het artikel over de annotatiebeoordelingsworkflow bekijkt dezelfde pagina vanuit de markeringskant. Gelijke tellingen op elk testbestand betekenen daarentegen dat je fixtures deze soort bug helemaal niet kunnen detecteren, en het eerlijke antwoord is een formulierfixture toe te voegen die een link en een memoblaadje draagt
De veld-opsommings-, focus- en annotatie-API's die hier zijn beschreven, worden geleverd met PDFium Component voor Delphi, C++Builder en Lazarus, waarvan de productpagina de volledige formulierveldreferentie bevat, inclusief het veldinformatie-record en de focus-accessors