V PDFium Component, komponentě VCL/LCL nad PDFium pro Delphi, C++Builder a Lazarus, není index pole formuláře indexem anotace. Stránka nese anotace Link, Text a Ink vedle svých widgetů, takže výčet polí musí filtrovat podle FPDFAnnot_GetSubtype a nabízet logický index od nuly, který se zpět na skutečnou pozici anotace mapuje až přímo u nativního volání
Chyba, která tohle odhaluje, je jasná na první pohled. Tester stiskne Tab ve vyplněném fakturačním formuláři a kurzor zmizí, protože fokus přešel na hypertextový odkaz v patičce. Nebo hůř: nestane se vůbec nic, váš kód zaznamená pole 3 jako zaostřené, panel UI se aktualizuje, a FORM_SetFocusedAnnot celou dobu potichu vracelo false. Oba příznaky pramení ze stejné konstrukční chyby a jeden z nich má pod sebou ještě druhou skrytou příčinu
Dva prostory indexů, které PDFium poskytuje
PDFium vystavuje nad stejnou stránkou dvě číslovací schémata a ta se shodují jen na dokumentech, které náhodou neobsahují nic jiného než widgety formuláře. První je index anotace: pozice v poli stránky /Annots, kterou počítá FPDFPage_GetAnnotCount a kterou bere FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Druhý je logický index pole, který by mělo nabízet API na úrovni aplikace, a ten běží od nuly přes interaktivní pole, ke kterým se uživatel skutečně může dostat. ISO 32000-1 §12.5.6.19 definuje widget anotace jako vizuální reprezentaci interaktivních polí formuláře a §12.7 definuje samotný formulář. Všechno ostatní na stránce je jiný podtyp s jinou sémantikou: anotace Link má cíl, anotace Ink má seznam tahů, anotace Text je poznámková samolepka. Nic z toho nepatří do počtu polí a nic z toho nemůže přijmout fokus formuláře. Přesto v poli /Annots sedí promíchané s widgety v pořadí, v jakém je zapsala produkující aplikace, což často není pořadí, které by naznačovalo cokoli jiného na dokumentu
Proč Tab skončí na hypertextovém odkazu místo dalšího pole?
Protože počet polí byl ve skutečnosti počet anotací. Původní implementace vracela FPDFPage_GetAnnotCount přímo z FormFieldCount, zatímco přístupový objekt informací o poli, pomůcka pro pořadí tabulátoru i pomůcka pro fokus zacházely se stejným celým číslem jako s pozicí widgetu. Na čisté stránce AcroForm se šesti widgety a ničím jiným se šest rovná šesti a každý test projde. Přidejte hypertextový odkaz v patičce a recenzní poznámku na okraji a počet nahlásí osm polí, indexy 6 a 7 se rozřeší na objekty, které nejsou formulářem, a Tab jde přímo do nich
Oprava na straně výčtu spočívá v tom, počítat podtypy, ne anotace. Otevřete každou anotaci, zeptejte se na její podtyp, ponechte widgety a zavřete handle v bloku finally, protože FPDFPage_GetAnnot vrací vlastněný handle, který se musí vrátit zpátky přes 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;
Všimněte si, co tohle záměrně nedělá. Neptá se ničeho prostředí form-fill a nepotřebuje handle formuláře, protože podtyp žije ve slovníku anotace a je čitelný ze samotné stránky. To má význam pro pořadí: počet je dostupný dřív, než jste se rozhodli, jestli si dokument vůbec zaslouží prostředí form-fill, což článek o JavaScriptu AcroForm a hostitelských událostech pokrývá jako bezpečnostní rozhodnutí, ne jako pohodlnost
Mapování logického indexu zpět na nativní hranici
Pravidlo, které brání oběma prostorům, aby do sebe prosakovaly, je jednoduché: logický index je jediné číslo, které přechází přes vaše veřejné API, a na index anotace se převádí až v poslední funkci před nativním voláním. Jediná mapovací pomůcka, kterou používá informace o poli, fokus, nastavovače příznaků i pořadí tabulátoru stejně, je to, co dělá toto pravidlo vynutitelným
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;
Dvě vlastnosti této pomůcky stojí za to říct otevřeně. Je to lineární průchod, takže naivní smyčka přes každé pole stojí kvadratický počet otevření anotací na stránce se stovkami widgetů; pokud vypisujete celou stránku, projděte anotace jednou a sbírejte handly widgetů za pochodu, místo abyste volali mapovací funkci pro každé pole. A vrací -1 místo toho, aby vyvolala výjimku, což nechává na volajícím rozhodnout, jestli je zastaralý index programátorská chyba hodná výjimky, nebo souběh, který lze ignorovat, například poté, co úprava odstranila anotaci, na kterou se stále odkazuje uložený seznam UI z cache
Proč FORM_SetFocusedAnnot selže na headless stránce?
Protože PDFium odmítá zaostřit widget, jehož zobrazení stránky nebylo nikdy označeno jako platné. FORM_SetFocusedAnnot převádí anotaci na zobrazení stránky uvnitř prostředí form-fill, a pokud toto zobrazení stránky neexistuje, vrátí false bez jakékoli diagnostiky. Samotná oprava mapování indexu proto vyřeší, že Tab přistane na hypertextovém odkazu, ale ponechá druhý příznak nedotčený: váš záznam logického fokusu říká pole 3, nativně zaostřený widget je pořád nic, a každý přístupový prvek postavený na nativním fokusu, zaostřený text, zaostřená hodnota, stav výběru z voleb, dál vrací prázdno. Zobrazení stránky vytváří FORM_OnAfterLoadPage a ruší FORM_OnBeforeClosePage. Ve vieweru postaveném kolem vizuálního ovládacího prvku se tato volání dějí jako součást zobrazování stránky, a proto tato chyba tak často vypadá jako chyba pouze v headless režimu: stejný kód, který funguje v GUI demu, selhává v dávkovém nástroji. Životní cyklus patří dokumentovému objektu, ne vieweru, takže PDFium Component teď vydává obě volání pokaždé, když se stránka načte nebo uvolní s přítomným handlem formuláře. C signatura bere jako první parametr stránku a jako druhý handle formuláře, což se při ručním psaní bindingu snadno prohodí
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;
Kontrola, která dokazuje opravu, je ta, co porovná obě strany. Zavolejte FocusFormField s logickým indexem a poté přečtěte hodnotu přes přístupový prvek, který jde přes nativně zaostřený widget, a ne přes váš vlastní záznam, jako FocusedFormFieldValue nebo FocusedFormOptionSelected. Pokud logický index projde tam a zpátky správně, ale nativní přístupový prvek se vrátí prázdný, chybí zobrazení stránky, ne mapování
Co logický index pole neslibuje
Index pole od nuly je pohodlí, ne sémantická identita, a z toho plynou čtyři omezení. Je vztažený ke stránce, ne k dokumentu, takže index 0 na stránce 2 je jiný widget než index 0 na stránce 1 a jejich porovnávání nemá smysl. Je poziční, takže vložení nebo smazání anotace zneplatní každý uložený index nad místem změny; s uloženým indexem zacházejte jako s platným jen tak dlouho, dokud stránka zůstává načtená a needitovaná
Třetí omezení je to, co překvapí lidi při kontrole seznamu polí. Index vyčísluje widgety, ne pole. Skupina přepínačů je jedno pole s několika potomky widgetů, takže tříprvková skupina přispívá třemi po sobě jdoucími indexy, které všechny hlásí stejné Name. Záznam TPdfFormFieldInfo nese GroupCount a GroupIndex přesně pro tento případ, a UI seznamu, které je ignoruje, zobrazí stejné pole třikrát. Čtvrté omezení se týká pořadí procházení: pořadí tabulátoru vystavené zde je pořadí výčtu widgetů, které následuje pole /Annots, ne položku stránky /Tabs (ISO 32000-1 §7.7.3.3) a ne strom polí AcroForm. U většiny producentů se tyto shodují; u formuláře rozvrženého do dvou sloupců generátorem, který nejdřív vypsal pravý sloupec, se neshodují, a cesta klávesnicí popsaná v článku o navigaci polí formuláře bude působit špatně, přestože každý index je správný. Když se zákaznický soubor chová divně, vypište oba prostory indexů vedle sebe, dřív než začnete teoretizovat: pohled na anotace a pohled na pole u stejné stránky, vytištěné společně, obvykle udělají příčinu zjevnou na jeden pohled
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;
Počet anotací výrazně vyšší než počet polí znamená, že stránka míchá podtypy, což je u kontrolovaných dokumentů normální a přesně pro tuto situaci mapování existuje; článek o workflow revize anotací se dívá na stejnou stránku ze strany poznámek. Stejné počty na každém testovacím souboru naopak znamenají, že vaše fixtures tuto třídu chyby vůbec nedokážou odhalit, a poctivá reakce je přidat fixture formuláře, který nese odkaz i poznámkovou samolepku
API pro výčet polí, fokus a anotace popsané zde je součástí PDFium Component pro Delphi, C++Builder a Lazarus, jehož produktová stránka nese kompletní referenci polí formuláře včetně záznamu informací o poli a přístupových prvků pro fokus