В PDFium Component, PDFium-базирания VCL/LCL компонент за Delphi, C++Builder, и Lazarus, form field индекс не е annotation индекс. Страница носи Link, Text, и Ink анотации до своите widget-и, така че изброяването на полета трябва да филтрира по FPDFAnnot_GetSubtype и да излага zero-based логически индекс, картографиран обратно към реална annotation позиция само при native извикването
Бъгът, разкриващ това, е недвусмислен, щом сте го виждали. Тестер натиска Tab във попълнен формуляр за фактура, и курсорът изчезва, защото фокусът отиде на hyperlink в footer-а. Или по-лошо, нищо не се случва изобщо: вашият код записва поле 3 като фокусирано, UI панелът се обновява, а FORM_SetFocusedAnnot тихо е връщал false през цялото време. И двата симптома идват от една и съща грешка в дизайна, и единият от тях има втора root cause, скрита отдолу
Двете пространства от индекси, които PDFium ви дава
PDFium излага две номериращи схеми над една и съща страница, и те съвпадат само за документи, случайно съдържащи само form widget-и. Първата е annotation индексът: позиция в масива на страница /Annots, което е това, което FPDFPage_GetAnnotCount брои и това, което FPDFPage_GetAnnot приема (ISO 32000-1 §12.5.2). Втората е логическият field индекс, който API на ниво приложение би трябвало да предлага, вървящ от нула над интерактивните полета, до които потребител действително може да достигне. ISO 32000-1 §12.5.6.19 дефинира widget анотации като визуалното представяне на интерактивни form полета, а §12.7 дефинира самия формуляр. Всичко останало на страницата е различен подтип с различна семантика: Link анотация има дестинация, Ink анотация има списък от щрихи, Text анотация е лепенка бележка. Никоя от тях не принадлежи на брой полета, и никоя не може да приеме form фокус. И все пак в масива /Annots те седят преплетени с widget-ите в какъвто и да е ред произвеждащото приложение да ги е записало, което често не е редът, който останалата част от документа предполага
Защо Tab кацва на hyperlink вместо на следващото поле?
Защото броят полета всъщност беше annotation брой. Оригиналната имплементация връщаше FPDFPage_GetAnnotCount директно от FormFieldCount, докато аксесорът за field информация, помощникът за tab ред, и помощникът за фокус, всички третираха същото цяло число като widget позиция. На чиста AcroForm страница с шест widget-а и нищо друго, шест е равно на шест, и всеки тест минава. Добавете hyperlink в footer-а и коментар на рецензент в margin-а, и броят отчита осем полета, индекси 6 и 7 се разрешават до не-form обекти, а Tab върви право в тях
Поправката на края за изброяване е да се броят подтипове, а не анотации. Отворете всяка анотация, попитайте за нейния подтип, задръжте widget-ите, и затворете handle-а в блок finally, защото FPDFPage_GetAnnot връща притежаван handle, който трябва да се върне обратно чрез 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;
Отбележете какво това умишлено не прави. Не пита нищо form-fill environment-а, и не се нуждае от form handle, защото подтипът живее в речника на анотацията и е четим само от страницата. Това има значение за подредбата: броят е наличен преди да сте решили дали документът изобщо заслужава form-fill environment, което статията за AcroForm JavaScript и host events покрива като решение за сигурност, а не за удобство
Картографиране на логическия индекс обратно на native границата
Правилото, пазещо двете пространства от изтичане едно в друго, е просто: логическият индекс е единственото число, преминаващо през публичното ви API, и се конвертира в annotation индекс в последната функция преди native извикването. Един помощник за картографиране, използван от field информация, фокус, flag setter-и, и tab ред еднакво, е това, което прави правилото приложимо
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;
Две свойства на този помощник си струва да се посочат ясно. Той е линейно сканиране, така че наивен цикъл над всяко поле струва квадратичен брой отваряния на анотации на страница със стотици widget-и; ако изброявате цялата страница, обходете анотациите веднъж и съберете widget handle-ите, докато вървите, вместо да извиквате mapper-а за поле. И връща -1, вместо да вдига грешка, което позволява на извикващия да реши дали остарял индекс е програмна грешка, заслужаваща изключение, или надпревара, заслужаваща игнориране, например след като редакция е премахнала анотация, до която кеширан UI списък все още се позовава
Защо FORM_SetFocusedAnnot се проваля на headless страница?
Защото PDFium отказва да фокусира widget, чийто page view никога не е бил маркиран валиден. FORM_SetFocusedAnnot разрешава анотацията до page view вътре в form-fill environment-а, и ако този page view не съществува, връща false без никаква диагностика. Поправянето само на индексното картографиране затова поправя кацането на Tab на hyperlink, но оставя втория симптом недокоснат: вашият запис за логически фокус казва поле 3, native фокусираният widget е все още нищо, и всеки accessor, изграден върху native фокуса, фокусиран текст, фокусирана стойност, състояние на избор в choice, продължава да връща празно. Page view-ът се създава от FORM_OnAfterLoadPage и унищожава от FORM_OnBeforeClosePage. Във viewer, изграден около visual control, тези извиквания се случват като част от показването на страница, поради което провалът толкова често изглежда като headless-само бъг: същият код, работещ в GUI демото, се проваля в batch инструмента. Жизненият цикъл принадлежи на document обекта, не на viewer-а, така че PDFium Component сега издава и двете извиквания винаги когато страница се зарежда или разтоварва с наличен form handle. C сигнатурата приема страницата първо, а form handle-а второ, което е лесно да се обърне при ръчно писане на 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;
Проверката, доказваща поправката, е тази, сравняваща двете страни. Извикайте FocusFormField с логически индекс, после прочетете стойност чрез accessor, минаващ през native фокусирания widget, а не през собствения ви запис, като FocusedFormFieldValue или FocusedFormOptionSelected. Ако логическият индекс се обръща правилно, но native accessor-ът връща празно, page view-ът липсва, не картографирането
Какво логическият field индекс не обещава
Zero-based field индекс е удобство, не семантична идентичност, и четири лимита произтичат от това. Той е за страница, не за документ, така че индекс 0 на страница 2 е различен widget от индекс 0 на страница 1, и сравняването им е безсмислено. Той е позиционен, така че вмъкване или изтриване на анотация анулира всеки кеширан индекс над промяната; третирайте съхранен индекс като валиден само докато страницата остава заредена и нередактирана
Третият лимит е този, изненадващ хора, преглеждащи списък с полета. Индексът изброява widget-и, не полета. Radio група е едно поле с няколко widget деца, така че group от три бутона допринася три последователни индекса, всички отчитащи същия Name. Записът TPdfFormFieldInfo носи GroupCount и GroupIndex точно за този случай, а списъчен UI, игнориращ ги, показва същото поле три пъти. Четвъртият лимит засяга реда на обхождане: tab редът, изложен тук, е редът на изброяване на widget-и, който следва масива /Annots, не записа /Tabs на страницата (ISO 32000-1 §7.7.3.3) и не дървото на полета в AcroForm. За повечето производители те съвпадат; за формуляр, оформен в две колони от генератор, излъчил дясната колона първо, не съвпадат, а пътят с клавиатура, описан в статията за навигация на form полета, ще се усети грешно, дори всеки индекс да е правилен. Когато файл на клиент се държи странно, дъмпнете двете пространства от индекси едно до друго преди да теоретизирате: гледната точка на анотации и гледната точка на полета на същата страница, отпечатани заедно, обикновено правят причината очевидна с един поглед
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;
Annotation брой далеч над field броя означава, че страницата смесва подтипове, което е нормално в рецензирани документи и е точно ситуацията, за която картографирането съществува; статията за workflow на преглед на анотации гледа на същата страница от markup страната. Равни бройки на всеки тестов файл, от друга страна, означават, че вашите фикстури изобщо не могат да засекат този клас бъг, а честният отговор е да добавите form фикстура, носеща линк и лепенка бележка
Изброяването на полета, фокусът, и annotation API-тата, описани тук, се доставят с PDFium Component за Delphi, C++Builder, и Lazarus, чиято продуктова страница носи пълния справочник за form-field, включително записа за field информация и accessor-ите за фокус