W PDFium Component, opartym na PDFium komponencie VCL/LCL dla Delphi, C++Buildera i Lazarusa, indeks pola formularza nie jest indeksem adnotacji. Strona niesie adnotacje Link, Text i Ink obok swoich widgetów, więc wyliczanie pól musi filtrować po FPDFAnnot_GetSubtype i wystawiać zerowo indeksowany indeks logiczny, mapowany z powrotem na rzeczywistą pozycję adnotacji dopiero przy wywołaniu natywnym
Błąd, który to ujawnia, jest jednoznaczny, gdy raz go zobaczysz. Tester naciska Tab w wypełnionym formularzu faktury, a kursor znika, ponieważ fokus przeszedł na hiperłącze w stopce. Albo gorzej: nic się nie dzieje, twój kod zapisuje pole 3 jako sfokusowane, panel interfejsu się aktualizuje, a FORM_SetFocusedAnnot po cichu zwracała false przez cały czas. Oba objawy wynikają z tego samego błędu projektowego, a jeden z nich ma drugą, ukrytą przyczynę źródłową
Dwie przestrzenie indeksów, które daje ci PDFium
PDFium wystawia dwa systemy numeracji nad tą samą stroną, i pokrywają się one tylko na dokumentach, które akurat nie zawierają niczego poza widgetami formularza. Pierwszy to indeks adnotacji: pozycja w tablicy strony /Annots, którą liczy FPDFPage_GetAnnotCount i którą przyjmuje FPDFPage_GetAnnot (ISO 32000-1 §12.5.2). Drugi to logiczny indeks pola, który API na poziomie aplikacji powinno oferować, liczony od zera po interaktywnych polach, do których użytkownik faktycznie może dotrzeć. ISO 32000-1 §12.5.6.19 definiuje widgety adnotacji jako wizualną reprezentację interaktywnych pól formularza, a §12.7 definiuje sam formularz. Wszystko inne na stronie to inny podtyp o innej semantyce: adnotacja Link ma cel, adnotacja Ink ma listę pociągnięć, adnotacja Text to karteczka samoprzylepna. Żadna z nich nie należy do liczby pól i żadna nie może przyjąć fokusu formularza. A jednak w tablicy /Annots siedzą przeplecione z widgetami w dowolnej kolejności, w jakiej zapisała je aplikacja produkująca, co często wcale nie jest kolejnością, jaką sugerowałoby cokolwiek innego w dokumencie
Dlaczego Tab ląduje na hiperłączu zamiast na kolejnym polu?
Ponieważ liczba pól była w rzeczywistości liczbą adnotacji. Oryginalna implementacja zwracała FPDFPage_GetAnnotCount bezpośrednio z FormFieldCount, podczas gdy akcesor informacji o polu, pomocnik kolejności tabulacji i pomocnik fokusu traktowały tę samą liczbę całkowitą jako pozycję widgetu. Na czystej stronie AcroForm z sześcioma widgetami i niczym więcej sześć równa się sześciu i każdy test przechodzi. Dodaj hiperłącze w stopce i komentarz recenzenta na marginesie, a liczba zgłasza osiem pól, indeksy 6 i 7 rozwiązują się do obiektów niebędących formularzem, a Tab wchodzi prosto w nie
Poprawka na końcu wyliczania polega na liczeniu podtypów zamiast adnotacji. Otwórz każdą adnotację, zapytaj o jej podtyp, zachowaj widgety i zamknij uchwyt w bloku finally, ponieważ FPDFPage_GetAnnot zwraca uchwyt na własność, który musi wrócić przez 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;
Zwróć uwagę na to, czego to celowo nie robi. Nie pyta niczego środowiska wypełniania formularza i nie potrzebuje uchwytu formularza, ponieważ podtyp żyje w słowniku adnotacji i jest odczytywalny wyłącznie ze strony. To ma znaczenie dla kolejności: liczba jest dostępna, zanim jeszcze zdecydowałeś, czy dokument w ogóle zasługuje na środowisko wypełniania formularza, co artykuł o JavaScript AcroForm i zdarzeniach hosta omawia jako decyzję bezpieczeństwa, a nie wygody
Mapowanie indeksu logicznego z powrotem na natywnej granicy
Reguła utrzymująca obie przestrzenie przed wzajemnym przeciekaniem jest prosta: indeks logiczny to jedyna liczba przekraczająca twoje publiczne API, i jest konwertowana na indeks adnotacji w ostatniej funkcji przed wywołaniem natywnym. Jeden pomocnik mapujący, używany zarówno przez informacje o polu, fokus, ustawiacze flag, jak i kolejność tabulacji, sprawia, że ta reguła jest egzekwowalna
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;
Dwie właściwości tego pomocnika warto wprost wypowiedzieć. To przeszukiwanie liniowe, więc naiwna pętla po każdym polu kosztuje kwadratową liczbę otwarć adnotacji na stronie z setkami widgetów; jeśli wyliczasz całą stronę, przejdź przez adnotacje raz i zbierz uchwyty widgetów po drodze zamiast wywoływać mapper na każde pole. I zwraca -1 zamiast rzucać, co pozwala wywołującemu zdecydować, czy nieaktualny indeks jest błędem programistycznym wartym wyjątku, czy wyścigiem wartym zignorowania, na przykład po edycji, która usunęła adnotację, do której wciąż odwołuje się buforowana lista interfejsu
Dlaczego FORM_SetFocusedAnnot zawodzi na stronie bez interfejsu?
Ponieważ PDFium odmawia sfokusowania widgetu, którego widok strony nigdy nie został oznaczony jako ważny. FORM_SetFocusedAnnot rozwiązuje adnotację do widoku strony wewnątrz środowiska wypełniania formularza, a jeśli ten widok strony nie istnieje, zwraca false bez żadnej diagnostyki. Sama poprawka mapowania indeksu naprawia więc lądowanie Tab na hiperłączu, ale pozostawia nietknięty drugi objaw: twój logiczny rekord fokusu mówi „pole 3", natywnie sfokusowany widget wciąż jest niczym, a każdy akcesor zbudowany na natywnym fokusie — sfokusowany tekst, sfokusowana wartość, stan wyboru z listy — wciąż zwraca pustkę. Widok strony jest tworzony przez FORM_OnAfterLoadPage i niszczony przez FORM_OnBeforeClosePage. W przeglądarce zbudowanej wokół kontrolki wizualnej te wywołania zdarzają się jako część wyświetlania strony, dlatego ta awaria tak często wygląda jak błąd wyłącznie bez interfejsu: ten sam kod, który działa w demie z GUI, zawodzi w narzędziu wsadowym. Cykl życia należy do obiektu dokumentu, nie do przeglądarki, więc PDFium Component wydaje teraz oba wywołania, ilekroć strona jest wczytywana lub zwalniana przy obecnym uchwycie formularza. Sygnatura C przyjmuje najpierw stronę, a potem uchwyt formularza, co łatwo odwrócić, pisząc wiązanie ręcznie
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;
Sprawdzenie dowodzące poprawki jest tym, które porównuje obie strony. Wywołaj FocusFormField z indeksem logicznym, a potem odczytaj wartość przez akcesor, który przechodzi przez natywnie sfokusowany widget, a nie przez twój własny rekord, jak FocusedFormFieldValue lub FocusedFormOptionSelected. Jeśli indeks logiczny wraca poprawnie, ale natywny akcesor zwraca pustkę, brakuje widoku strony, nie mapowania
Czego logiczny indeks pola nie obiecuje
Zerowo indeksowany indeks pola to wygoda, nie tożsamość semantyczna, i wynikają z tego cztery ograniczenia. Jest na poziomie strony, nie dokumentu, więc indeks 0 na stronie 2 to inny widget niż indeks 0 na stronie 1, a porównywanie ich jest bez znaczenia. Jest pozycyjny, więc wstawienie lub usunięcie adnotacji unieważnia każdy zbuforowany indeks powyżej zmiany; traktuj przechowywany indeks jako ważny tylko tak długo, jak strona pozostaje wczytana i nieedytowana
Trzecie ograniczenie to to, które zaskakuje przy przeglądaniu listy pól. Indeks wylicza widgety, nie pola. Grupa przycisków radiowych to jedno pole z kilkoma widgetami-dziećmi, więc trzyprzyciskowa grupa wnosi trzy kolejne indeksy, które wszystkie zgłaszają tę samą Name. Rekord TPdfFormFieldInfo niesie GroupCount i GroupIndex dokładnie na ten przypadek, a interfejs listy, który je ignoruje, pokazuje to samo pole trzykrotnie. Czwarte ograniczenie dotyczy kolejności przechodzenia: kolejność tabulacji wystawiana tutaj to kolejność wyliczania widgetów, która podąża za tablicą /Annots, nie za wpisem strony /Tabs (ISO 32000-1 §7.7.3.3) ani drzewem pól AcroForm. Dla większości producentów te się zgadzają; dla formularza ułożonego w dwie kolumny przez generator, który zapisał najpierw prawą kolumnę, nie zgadzają się, a ścieżka klawiaturowa opisana w artykule o nawigacji po polach formularza będzie sprawiać wrażenie błędnej, mimo że każdy indeks jest poprawny. Gdy plik klienta zachowuje się dziwnie, zrzuć obie przestrzenie indeksów obok siebie, zanim zaczniesz teoretyzować: widok adnotacji i widok pola tej samej strony, wydrukowane razem, zwykle czynią przyczynę oczywistą na jeden rzut oka
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;
Liczba adnotacji znacznie przewyższająca liczbę pól oznacza, że strona miesza podtypy, co jest normalne w recenzowanych dokumentach i dokładnie sytuacją, dla której istnieje to mapowanie; artykuł o przepływie pracy przeglądu adnotacji patrzy na tę samą stronę od strony oznaczeń. Równe liczby na każdym pliku testowym, z drugiej strony, oznaczają, że twoje fixture'y w ogóle nie potrafią wykryć tej klasy błędu, a uczciwą odpowiedzią jest dodanie fixture'u formularza niosącego łącze i karteczkę samoprzylepną
Opisane tutaj API wyliczania pól, fokusu i adnotacji są dostarczane z PDFium Component dla Delphi, C++Buildera i Lazarusa, którego strona produktu niesie pełne odniesienie pól formularza, w tym rekord informacji o polu i akcesory fokusu