Tehnički članak

Indeks widgeta vs indeks napomene u PDFium Delphi obrascima

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