Tekninen artikkeli

Widget-indeksi vs huomautusindeksi PDFium Delphi-lomakkeissa

PDFium Componentissa, PDFium-pohjaisessa VCL/LCL-komponentissa Delphille, C++Builderille ja Lazarukselle, lomakekentän indeksi ei ole huomautusindeksi. Sivu kantaa Link-, Text- ja Ink-huomautuksia widgettiensä vierellä, joten kenttien luettelointi täytyy suodattaa FPDFAnnot_GetSubtype:lla ja paljastaa nollapohjainen looginen indeksi, kartoitettuna takaisin todelliseen huomautuspaikkaan vasta natiivikutsussa

Bugi, joka paljastaa tämän, on erehtymätön, kun sen on kerran nähnyt. Testaaja painaa Tab-näppäintä täytetyssä laskulomakkeessa, ja kohdistin katoaa, koska fokus meni alatunnisteen hyperlinkkiin. Tai pahempaa, mitään ei tapahdu ollenkaan: koodisi tallentaa kentän 3 fokusoituna, käyttöliittymäpaneeli päivittyy, ja FORM_SetFocusedAnnot palautti hiljaa false:n koko ajan. Molemmat oireet tulevat samasta suunnitteluvirheestä, ja yhdellä niistä on toinen juurisyy piilossa alla

Kaksi indeksiavaruutta, jotka PDFium antaa sinulle

PDFium paljastaa kaksi numerointijärjestelmää saman sivun yli, ja ne täsmäävät vain dokumenteilla, jotka sattuvat sisältämään vain lomakewidgettejä. Ensimmäinen on huomautusindeksi: paikka sivun /Annots-taulukossa, jonka FPDFPage_GetAnnotCount laskee ja jonka FPDFPage_GetAnnot ottaa (ISO 32000-1 §12.5.2). Toinen on looginen kenttäindeksi, jonka sovellustason API:n pitäisi tarjota, kulkien nollasta interaktiivisten kenttien yli, jotka käyttäjä oikeasti pystyy tavoittamaan. ISO 32000-1 §12.5.6.19 määrittelee widget-huomautukset interaktiivisten lomakekenttien visuaalisena esityksenä, ja §12.7 määrittelee itse lomakkeen. Kaikki muu sivulla on eri alityyppi eri semantiikalla: Link-huomautuksella on kohde, Ink-huomautuksella on vetoluettelo, Text-huomautus on tarralappu. Mikään niistä ei kuulu kenttälaskuun, eikä mikään niistä pysty ottamaan vastaan lomakefokusta. Silti /Annots-taulukossa ne istuvat sekoittuneina widgettien kanssa missä tahansa järjestyksessä, jonka tuottava sovellus kirjoitti ne, mikä on usein eri järjestys kuin mitä mikään muu dokumentissa ehdottaisi

Miksi Tab laskeutuu hyperlinkkiin seuraavan kentän sijaan?

Koska kenttälaskenta oli oikeasti huomautuslaskenta. Alkuperäinen toteutus palautti FPDFPage_GetAnnotCount:in suoraan FormFieldCount:ista, samalla kun kenttätietojen käyttäjä, sarkainjärjestyksen apuri ja fokusapuri kaikki käsittelivät tuota samaa kokonaislukua widget-paikkana. Puhtaalla AcroForm-sivulla kuudella widgetillä eikä muulla, kuusi on yhtä kuin kuusi ja jokainen testi läpäisee. Lisää hyperlinkki alatunnisteeseen ja tarkastajan kommentti marginaaliin, ja laskenta raportoi kahdeksan kenttää, indeksit 6 ja 7 ratkeavat ei-lomakeobjekteiksi, ja Tab kävelee suoraan niihin

Korjaus luettelointipäässä on laskea alityyppejä eikä huomautuksia. Avaa jokainen huomautus, kysy sen alityyppiä, pidä widgetit, ja sulje kahva finally-lohkossa, koska FPDFPage_GetAnnot palauttaa omistetun kahvan, jonka täytyy mennä takaisin FPDFPage_CloseAnnot:in kautta

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;

Huomaa, mitä tämä tarkoituksella ei tee. Se ei kysy lomaketäyttöympäristöltä mitään, eikä se tarvitse lomakekahvaa, koska alityyppi elää huomautussanakirjassa ja on luettavissa pelkästä sivusta. Se on tärkeää järjestyksen kannalta: laskenta on saatavilla ennen kuin olet edes päättänyt, ansaitseeko dokumentti ylipäätään lomaketäyttöympäristön, mikä käsitellään artikkelissa AcroForm JavaScriptista ja isäntätapahtumista turvallisuuspäätöksenä eikä mukavuuspäätöksenä

Loogisen indeksin kartoittaminen takaisin natiivirajalla

Sääntö, joka estää kahta avaruutta vuotamasta toisiinsa, on yksinkertainen: looginen indeksi on ainoa luku, joka ylittää julkisen API:si, ja se muunnetaan huomautusindeksiksi viimeisessä funktiossa ennen natiivikutsua. Yksi kartoitusapuri, jota kenttätiedot, fokus, lippujen asetusfunktiot ja sarkainjärjestys kaikki käyttävät, on se, mikä tekee tuosta säännöstä täytäntöönpanokelpoisen

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;

Kaksi tämän apurin ominaisuutta kannattaa todeta suoraan. Se on lineaarinen skannaus, joten naiivi silmukka jokaisen kentän yli maksaa neliöllisen määrän huomautusavauksia sivulla, jolla on satoja widgettejä; jos luettelet koko sivua, kulje huomautukset läpi kerran ja kerää widget-kahvat matkan varrella sen sijaan, että kutsuisit kartoittajaa jokaista kenttää kohti. Ja se palauttaa -1:n nostamisen sijaan, mikä antaa kutsujalle mahdollisuuden päättää, onko vanhentunut indeksi ohjelmointivirhe, joka ansaitsee poikkeuksen, vai kilpajuoksu, joka ansaitsee ohituksen, esimerkiksi muokkauksen jälkeen, joka poisti huomautuksen, johon välimuistiin tallennettu käyttöliittymälista yhä viittaa

Miksi FORM_SetFocusedAnnot epäonnistuu ilman käyttöliittymää ajettavalla sivulla?

Koska PDFium kieltäytyy fokusoimasta widgettiä, jonka sivunäkymää ei koskaan merkitty päteväksi. FORM_SetFocusedAnnot ratkaisee huomautuksen sivunäkymäksi lomaketäyttöympäristön sisällä, ja jos tuota sivunäkymää ei ole olemassa, se palauttaa false:n ilman minkäänlaista diagnostiikkaa. Pelkän indeksikartoituksen korjaaminen siis korjaa Tab:n laskeutumisen hyperlinkkiin, mutta jättää toisen oireen koskemattomaksi: loogisen fokuksesi tietue sanoo kenttä 3, natiivi fokusoitu widget on yhä tyhjä, ja jokainen natiivin fokuksen päälle rakennettu käyttäjä, fokusoitu teksti, fokusoitu arvo, valinnan tilan tila, jatkaa tyhjän palauttamista. Sivunäkymän luo FORM_OnAfterLoadPage ja tuhoaa FORM_OnBeforeClosePage. Visuaalisen kontrollin ympärille rakennetussa katseluohjelmassa nuo kutsut tapahtuvat osana sivun näyttämistä, minkä vuoksi epäonnistuminen niin usein näyttää vain-ilman-käyttöliittymää-bugilta: sama koodi, joka toimii GUI-demossa, epäonnistuu eräajotyökalussa. Elinkaari kuuluu dokumenttiobjektille, ei katseluohjelmalle, joten PDFium Component antaa nyt molemmat kutsut aina, kun sivu ladataan tai puretaan lomakekahvan ollessa läsnä. C-signatuuri ottaa sivun ensin ja lomakekahvan toisena, mikä on helppo kääntää ympäri, kun kirjoitat sidontaa käsin

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;

Tarkistus, joka todistaa korjauksen, on se, joka vertaa kahta puolta. Kutsu FocusFormField:ia loogisella indeksillä, lue sitten arvo käyttäjän kautta, joka kulkee natiivin fokusoidun widgetin kautta oman tietueesi sijaan, kuten FocusedFormFieldValue tai FocusedFormOptionSelected. Jos looginen indeksi kiertää edestakaisin mutta natiivi käyttäjä tulee takaisin tyhjänä, sivunäkymä puuttuu, ei kartoitus

Mitä looginen kenttäindeksi ei lupaa

Nollapohjainen kenttäindeksi on mukavuus, ei semanttinen identiteetti, ja neljä rajaa seuraa siitä. Se on sivukohtainen, ei dokumenttikohtainen, joten indeksi 0 sivulla 2 on eri widget kuin indeksi 0 sivulla 1, ja niiden vertaaminen on merkityksetöntä. Se on paikkaan sidottu, joten huomautuksen lisääminen tai poistaminen mitätöi jokaisen välimuistiin tallennetun indeksin muutoksen yläpuolella; kohtele tallennettua indeksiä pätevänä vain niin kauan kuin sivu pysyy ladattuna ja muokkaamattomana

Kolmas raja on se, joka yllättää kenttälistaa tarkastelevat ihmiset. Indeksi luetteloi widgettejä, ei kenttiä. Radioryhmä on yksi kenttä useilla widget-lapsilla, joten kolmen painikkeen ryhmä tuottaa kolme peräkkäistä indeksiä, jotka kaikki raportoivat saman Name:n. TPdfFormFieldInfo-tietue kantaa GroupCount:in ja GroupIndex:in täsmälleen tätä tapausta varten, ja käyttöliittymälista, joka ohittaa ne, näyttää saman kentän kolmesti. Neljäs raja koskee läpikäyntijärjestystä: täällä paljastettu sarkainjärjestys on widget-luettelointijärjestys, joka seuraa /Annots-taulukkoa, ei sivun /Tabs-merkintää (ISO 32000-1 §7.7.3.3) eikä AcroForm-kenttäpuuta. Useimmille tuottajille nämä täsmäävät; lomakkeelle, joka on aseteltu kahteen sarakkeeseen generaattorilla, joka tuotti oikean sarakkeen ensin, ne eivät täsmää, ja näppäimistöpolku, joka kuvataan artikkelissa lomakekentän navigoinnista, tuntuu väärältä, vaikka jokainen indeksi on oikein. Kun asiakastiedosto käyttäytyy oudosti, dumppaa molemmat indeksiavaruudet rinnakkain ennen teorioimista: saman sivun huomautusnäkymä ja kenttänäkymä, tulostettuina yhdessä, tekevät yleensä syyn ilmeiseksi yhdellä silmäyksellä

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;

Huomautuslaskenta, joka on paljon kenttälaskentaa korkeampi, tarkoittaa, että sivu sekoittaa alityyppejä, mikä on normaalia tarkastetuissa dokumenteissa ja on täsmälleen se tilanne, jota varten kartoitus on olemassa; artikkeli huomautusten tarkastustyönkulusta katsoo samaa sivua merkintäpuolelta. Yhtäläiset laskennat jokaisessa testitiedostossa taas tarkoittavat, että kalusteesi eivät pysty havaitsemaan tätä bugiluokkaa lainkaan, ja rehellinen vastaus on lisätä lomakekaluste, joka kantaa linkin ja tarralapun

Tässä kuvatut kenttäluettelointi-, fokus- ja huomautus-API:t toimitetaan PDFium Componentin mukana Delphille, C++Builderille ja Lazarukselle, jonka tuotesivu sisältää täydellisen lomakekenttäviitteen, mukaan lukien kenttätietotietueen ja fokuskäyttäjät