U PDFium Component, indeks polja obrasca nije indeks napomene. Stranica nosi napomene Link, Text i Ink uz svoje widgete, pa nabrajanje polja mora filtrirati po FPDFAnnot_GetSubtype i izložiti logički indeks s bazom nula, mapiran natrag na pravu poziciju napomene tek pri nativnom pozivu
Bug koji ovo otkriva je nepogrešiv čim ga jednom vidite. Testeru koji pritisne Tab u popunjenom obrascu fakture kursor nestane, jer je fokus otišao na hipervezu u podnožju. Ili gore, ništa se ne dogodi: vaš kôd zabilježi polje 3 kao fokusirano, UI panel se ažurira, a FORM_SetFocusedAnnot tiho je vraćao false cijelo vrijeme. Oba simptoma dolaze iz iste projektne pogreške, a jedan od njih ima drugi skriveni korijenski uzrok
Dva prostora indeksa koje PDFium daje
PDFium izlaže dvije sheme numeriranja preko iste stranice, i one se podudaraju samo na dokumentima koji slučajno ne sadrže ništa osim widgeta obrazaca. Prvi je indeks napomene: pozicija u polju stranice /Annots, koju broji FPDFPage_GetAnnotCount, a koju uzima FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Drugi je logički indeks polja koji bi API na razini aplikacije trebao ponuditi, brojeći od nule preko interaktivnih polja koja korisnik zapravo može dosegnuti. ISO 32000-1 §12.5.6.19 definira widget napomene kao vizualni prikaz interaktivnih polja obrasca, a §12.7 definira sam obrazac. Sve ostalo na stranici je drukčiji podtip s drukčijom semantikom: napomena Link ima odredište, napomena Ink ima popis poteza, napomena Text je ljepljiva bilješka. Nijedna od njih ne pripada broju polja, i nijedna ne može primiti fokus obrasca. Ipak u polju /Annots sjede isprepletene s widgetima bilo kojim redoslijedom kojim ih je aplikacija koja ih proizvodi zapisala, što je često redoslijed koji ništa drugo o dokumentu ne sugerira
Zašto Tab slijeće na hipervezu umjesto na sljedeće polje?
Zato što je broj polja zapravo bio broj napomena. Izvorna implementacija vraćala je FPDFPage_GetAnnotCount izravno iz FormFieldCount, dok su pristupnik informacija o polju, pomoćnik redoslijeda tabulatora i pomoćnik fokusa svi tretirali taj isti cijeli broj kao poziciju widgeta. Na čistoj AcroForm stranici sa šest widgeta i ničim drugim, šest je jednako šest i svaki test prolazi. Dodajte hipervezu u podnožju i recenzentski komentar na margini, i broj prijavljuje osam polja, indeksi 6 i 7 razrješavaju se u objekte koji nisu obrazac, i Tab hoda ravno u njih
Popravak na kraju nabrajanja je brojati podtipove umjesto napomena. Otvorite svaku napomenu, pitajte za njezin podtip, zadržite widgete, i zatvorite ručku u bloku finally, jer FPDFPage_GetAnnot vraća posjedovanu ručku koja se mora vratiti 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;
Primijetite što ovo namjerno ne radi. Ne pita okruženje popunjavanja obrasca ništa, i ne treba ručku obrasca, jer podtip živi u rječniku napomene i čitljiv je sa same stranice. To je bitno za redoslijed: broj je dostupan prije nego ste odlučili treba li dokumentu uopće okruženje popunjavanja obrasca, što članak o AcroForm JavaScriptu i događajima domaćina pokriva kao sigurnosnu odluku, a ne kao pogodnost
Mapiranje logičkog indeksa natrag na nativnoj granici
Pravilo koje sprječava dva prostora da procure jedan u drugi je jednostavno: logički indeks je jedini broj koji prelazi vaš javni API, i pretvara se u indeks napomene u posljednjoj funkciji prije nativnog poziva. Jedan pomoćnik mapiranja, korišten za informacije o polju, fokus, postavljače zastavica i redoslijed tabulatora podjednako, ono je što čini to pravilo provedivim
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;
Dva svojstva ovog pomoćnika vrijedi jasno navesti. To je linearno pretraživanje, pa naivna petlja preko svakog polja košta kvadratni broj otvaranja napomena na stranici sa stotinama widgeta; ako nabrajate cijelu stranicu, prošetajte napomene jednom i skupite ručke widgeta usput umjesto da pozivate mapper po polju. I vraća -1 umjesto da baci iznimku, što pozivatelju dopušta da odluči je li zastarjeli indeks programska pogreška vrijedna iznimke ili utrka vrijedna ignoriranja, na primjer nakon što je uređivanje uklonilo napomenu na koju predmemorirani UI popis i dalje upućuje
Zašto FORM_SetFocusedAnnot ne uspije na stranici bez GUI-ja?
Zato što PDFium odbija fokusirati widget čiji prikaz stranice nikad nije označen valjanim. FORM_SetFocusedAnnot razrješava napomenu u prikaz stranice unutar okruženja popunjavanja obrasca, i ako taj prikaz stranice ne postoji, vraća false bez ikakve dijagnostike. Ispravljanje mapiranja indeksa samo po sebi stoga popravlja Tab koji slijeće na hipervezu, ali ostavlja drugi simptom netaknutim: vaš zapis logičkog fokusa kaže polje 3, nativno fokusirani widget je i dalje ništa, i svaki pristupnik izgrađen na nativnom fokusu, fokusirani tekst, fokusirana vrijednost, stanje odabira izbora, i dalje vraća prazno. Prikaz stranice stvara FORM_OnAfterLoadPage, a uništava FORM_OnBeforeClosePage. U prikazniku izgrađenom oko vizualne kontrole ti se pozivi događaju kao dio prikazivanja stranice, što je razlog zašto neuspjeh tako često izgleda kao bug samo bez GUI-ja: isti kôd koji radi u GUI demou ne uspijeva u alatu za obradu u skupinama. Životni ciklus pripada objektu dokumenta, a ne prikazniku, pa PDFium Component sada izdaje oba poziva kad god se stranica učita ili istovari s prisutnom ručkom obrasca. C potpis uzima prvo stranicu, a zatim ručku obrasca, što je lako obrnuti pri ručnom pisanju veze
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;
Provjera koja dokazuje popravak je ona koja uspoređuje dvije strane. Pozovite FocusFormField s logičkim indeksom, zatim pročitajte vrijednost kroz pristupnik koji ide kroz nativno fokusirani widget, a ne kroz vaš vlastiti zapis, poput FocusedFormFieldValue ili FocusedFormOptionSelected. Ako se logički indeks vrati round-trip, ali nativni pristupnik vrati prazno, prikaz stranice nedostaje, ne mapiranje
Što logički indeks polja ne obećava
Indeks polja s bazom nula je pogodnost, ne semantički identitet, a iz toga slijede četiri ograničenja. Po stranici je, ne po dokumentu, pa je indeks 0 na stranici 2 drukčiji widget od indeksa 0 na stranici 1, a njihova usporedba je besmislena. Pozicijski je, pa umetanje ili brisanje napomene poništava svaki predmemorirani indeks iznad promjene; tretirajte pohranjeni indeks kao valjan samo dok stranica ostane učitana i neuređena
Treće ograničenje je ono koje iznenadi ljude koji pregledavaju popis polja. Indeks nabraja widgete, ne polja. Radio grupa je jedno polje s nekoliko widget djece, pa grupa s tri gumba pridonosi tri uzastopna indeksa koja svi prijavljuju isto Name. Zapis TPdfFormFieldInfo nosi GroupCount i GroupIndex upravo za taj slučaj, a UI popis koji ih ignorira prikazuje isto polje tri puta. Četvrto ograničenje tiče se redoslijeda prolaska: redoslijed tabulatora izložen ovdje je redoslijed nabrajanja widgeta, koji slijedi polje /Annots, ne unos stranice /Tabs (ISO 32000-1 §7.7.3.3) i ne stablo polja AcroForma. Za većinu proizvođača ta se dva slažu; za obrazac raspoređen u dva stupca generatorom koji je emitirao desni stupac prvi, ne slažu se, a put tipkovnicom opisan u članku o navigaciji poljima obrasca osjećat će se pogrešno iako je svaki indeks ispravan. Kad se klijentova datoteka čudno ponaša, ispišite oba prostora indeksa jedan pored drugoga prije teoretiziranja: prikaz napomena i prikaz polja iste stranice, ispisani zajedno, obično čine uzrok očitim u jednom pogledu
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 napomena znatno iznad broja polja znači da stranica miješa podtipove, što je normalno u recenziranim dokumentima i upravo je situacija za koju mapiranje postoji; članak o radnom procesu revizije napomena gleda istu stranicu sa strane oznaka. Jednaki brojevi na svakoj testnoj datoteci, s druge strane, znače da vaše fixture ne mogu uopće otkriti ovu klasu buga, a pošten odgovor je dodati fixture obrasca koji nosi vezu i ljepljivu bilješku
Nabrajanje polja, fokus i API-ji napomena opisani ovdje isporučuju se s PDFium Component za Delphi, C++Builder i Lazarus, čija produktna stranica sadrži potpunu referencu polja obrasca uključujući zapis informacija o polju i pristupnike fokusa