U PDFium Component, VCL/LCL komponenti baziranoj na PDFium-u za Delphi, C++Builder, i Lazarus, indeks form polja nije indeks anotacije. Stranica nosi Link, Text, i Ink anotacije pored svojih widgeta, pa nabrajanje polja mora filtrirati po FPDFAnnot_GetSubtype i izložiti logički indeks od nule, mapiran nazad na stvarnu poziciju anotacije samo na nativnom pozivu
Bag koji ovo otkriva je nepogrešiv čim ga jednom vidite. Tester pritisne Tab u popunjenom formularu fakture i kursor nestane, jer je fokus otišao na hiperlink u podnožju. Ili gore, ne desi se ništa: vaš kod beleži polje 3 kao fokusirano, UI panel se ažurira, a FORM_SetFocusedAnnot je čitavo vreme tiho vraćao false. Oba simptoma dolaze iz iste greške u dizajnu, a jedan od njih ima drugi koreni uzrok skriven ispod
Dva prostora indeksa koje vam PDFium daje
PDFium izlaže dve šeme numeracije preko iste stranice, i one se poklapaju samo na dokumentima koji slučajno ne sadrže ništa osim form widgeta. Prvi je indeks anotacije: pozicija u nizu stranice /Annots, što je ono što FPDFPage_GetAnnotCount broji i što FPDFPage_GetAnnot uzima (ISO 32000-1 §12.5.2). Drugi je logički indeks polja koji bi API na nivou aplikacije trebalo da ponudi, koji ide od nule preko interaktivnih polja koja korisnik zaista može dosegnuti. ISO 32000-1 §12.5.6.19 definiše widget anotacije kao vizuelnu reprezentaciju interaktivnih form polja, a §12.7 definiše sam formular. Sve ostalo na stranici je drugačiji podtip sa drugačijom semantikom: Link anotacija ima destinaciju, Ink anotacija ima listu poteza, Text anotacija je lepljiva beleška. Nijedna od njih ne pripada brojanju polja, i nijedna ne može prihvatiti fokus formulara. Ipak u nizu /Annots sede isprepletane sa widgetima kojim god redosledom ih je aplikacija koja ih je proizvela zapisala, što često nije redosled koji išta drugo u vezi dokumenta sugeriše
Zašto Tab sleti na hiperlink umesto na sledeće polje?
Zato što je broj polja zapravo bio broj anotacija. Originalna implementacija je vraćala FPDFPage_GetAnnotCount direktno iz FormFieldCount, dok su pristupnik informacija o polju, helper redosleda tabulacije, i helper fokusa svi tretirali taj isti ceo broj kao poziciju widgeta. Na čistoj AcroForm stranici sa šest widgeta i ničim drugim, šest je jednako šest i svaki test prolazi. Dodajte hiperlink u podnožju i komentar recenzenta u margini, i broj prijavljuje osam polja, indeksi 6 i 7 se razrešavaju u objekte koji nisu forma, i Tab hoda pravo u njih
Popravka na strani nabrajanja je da se broje podtipovi, ne anotacije. Otvorite svaku anotaciju, pitajte za njen podtip, zadržite widgete, i zatvorite handle u finally bloku, jer FPDFPage_GetAnnot vraća handle u vlasništvu koji mora otići nazad kroz 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;
Obratite pažnju šta ovo namerno ne radi. Ne pita ništa okruženje za popunjavanje formulara, i ne treba mu form handle, jer podtip živi u rečniku anotacije i čitljiv je iz same stranice. To je bitno za redosled: broj je dostupan pre nego što ste odlučili da li dokument uopšte zaslužuje okruženje za popunjavanje formulara, što članak o AcroForm JavaScript-u i host eventima pokriva kao odluku bezbednosti, ne pogodnosti
Mapiranje logičkog indeksa nazad na nativnoj granici
Pravilo koje sprečava ta dva prostora da se izmešaju je jednostavno: logički indeks je jedini broj koji prelazi vaš javni API, i konvertuje se u indeks anotacije u poslednjoj funkciji pre nativnog poziva. Jedan helper mapiranja, korišćen podjednako od strane informacija o polju, fokusa, setera zastavica, i redosleda tabulacije, je ono što to pravilo čini sprovodivim
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;
Dve osobine ovog helpera vrede jasnog navođenja. To je linearni sken, pa naivna petlja preko svakog polja košta kvadratan broj otvaranja anotacija na stranici sa stotinama widgeta; ako nabrajate celu stranicu, obiđite anotacije jednom i skupljajte handle-ove widgeta usput umesto da pozivate mapper po polju. I vraća -1 umesto da baci grešku, što pozivaocu dozvoljava da odluči da li je zastareo indeks programerska greška vredna izuzetka ili trka vredna ignorisanja, na primer posle izmene koja je uklonila anotaciju na koju keširana UI lista i dalje referiše
Zašto FORM_SetFocusedAnnot otkazuje na headless stranici?
Zato što PDFium odbija da fokusira widget čiji prikaz stranice nikad nije bio obeležen kao validan. FORM_SetFocusedAnnot razrešava anotaciju u prikaz stranice unutar okruženja za popunjavanje formulara, a ako taj prikaz stranice ne postoji vraća false bez ikakve dijagnostike. Ispravljanje samog mapiranja indeksa zato popravlja da Tab sleti na hiperlink, ali ostavlja drugi simptom netaknut: vaš zapis logičkog fokusa kaže polje 3, nativno fokusiran widget je i dalje ništa, i svaki pristupnik izgrađen na nativnom fokusu, fokusiranom tekstu, fokusiranoj vrednosti, stanju izbora opcije, nastavlja da vraća prazno. Prikaz stranice kreira FORM_OnAfterLoadPage, a uništava FORM_OnBeforeClosePage. U vieweru izgrađenom oko vizuelne kontrole ti pozivi se dešavaju kao deo prikazivanja stranice, zato greška tako često izgleda kao bag samo za headless slučaj: isti kod koji radi u GUI demou otkazuje u batch alatu. Životni ciklus pripada objektu dokumenta, ne vieweru, pa PDFium Component sada izdaje oba poziva kad god se stranica učita ili istovari sa prisutnim form handle-om. C potpis uzima prvo stranicu, a form handle drugo, što je lako obrnuti kada ručno pišete 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;
Provera koja dokazuje popravku je ona koja upoređuje dve strane. Pozovite FocusFormField sa logičkim indeksom, zatim pročitajte vrednost kroz pristupnik koji ide preko nativno fokusiranog widgeta umesto preko vašeg sopstvenog zapisa, poput FocusedFormFieldValue ili FocusedFormOptionSelected. Ako logički indeks prođe round-trip, ali se nativni pristupnik vrati prazan, nedostaje prikaz stranice, ne mapiranje
Šta logički indeks polja ne obećava
Indeks polja od nule je pogodnost, ne semantički identitet, i iz toga slede četiri ograničenja. Po stranici je, ne po dokumentu, pa je indeks 0 na stranici 2 drugačiji widget od indeksa 0 na stranici 1, i njihovo poređenje je besmisleno. Pozicioni je, pa ubacivanje ili brisanje anotacije poništava svaki keširan indeks iznad izmene; tretirajte uskladišten indeks kao validan samo dok stranica ostaje učitana i neizmenjena
Treće ograničenje je ono koje iznenadi ljude koji pregledaju listu polja. Indeks nabraja widgete, ne polja. Radio grupa je jedno polje sa nekoliko widget dece, pa grupa od tri dugmeta doprinosi tri uzastopna indeksa koji svi prijavljuju isti Name. Zapis TPdfFormFieldInfo nosi GroupCount i GroupIndex tačno za ovaj slučaj, a lista UI koja ih ignoriše prikaže isto polje tri puta. Četvrto ograničenje se tiče redosleda obilaska: redosled tabulacije izložen ovde je redosled nabrajanja widgeta, koji prati niz /Annots, ne stavku /Tabs stranice (ISO 32000-1 §7.7.3.3) i ne stablo polja AcroForm-a. Za većinu proizvođača se ti redosledi poklapaju; za formular raspoređen u dve kolone od strane generatora koji je prvo emitovao desnu kolonu, ne poklapaju se, i putanja tastature opisana u članku o navigaciji form polja će delovati pogrešno iako je svaki indeks ispravan. Kada se korisnički fajl ponaša čudno, izbacite oba prostora indeksa jedan pored drugog pre teoretisanja: prikaz anotacija i prikaz polja iste stranice, ispisani zajedno, obično čine uzrok očiglednim na jedan pogled
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;
Broj anotacija daleko iznad broja polja znači da stranica meša podtipove, što je normalno u recenziranim dokumentima i tačno je situacija za koju mapiranje postoji; članak o workflow-u recenzije anotacija posmatra istu stranicu sa strane markupa. Jednaki brojevi na svakom test fajlu, s druge strane, znače da vaši fixturi uopšte ne mogu detektovati ovu klasu baga, i iskren odgovor je da dodate fixture formulara koji nosi link i lepljivu belešku
Nabrajanje polja, fokus, i API-ji anotacija opisani ovde isporučuju se sa PDFium Component za Delphi, C++Builder, i Lazarus, čija proizvodna stranica nosi kompletnu referencu form polja uključujući zapis informacija o polju i pristupnike fokusa