Teknisk artikel

PDFium: form-field navigation and viewer validation

Tryck Tab i ett PDF-formulär din kod byggde, och markören hamnar två fält bort från där den borde, eller hoppar över den andra kolumnen helt, eller hoppar tillbaka till toppen efter det tredje fältet i stället för det fjärde. Personen som fyller i en faktura i din visare förväntar sig att tangentbordet vandrar genom formuläret på samma sätt som det vandrar genom varje webbformulär de någonsin använt. När det inte gör det tar de till musen, jagar efter nästa ruta, och bestämmer sig tyst för att ditt verktyg är oavslutat. Förutsägbar fältnavigering är skillnaden mellan en dataregistreringsvisare folk tolererar och en de litar på, och det är nästan uteslutande en fråga om att använda rätt fokus-API i stället för att fejka tangentbordsinmatning med simulerade klick

Exemplen nedan använder PDFium Component, en PDFium-baserad VCL/LCL-komponent för Delphi, C++Builder och Lazarus. Navigering är en av tre saker en formulärvisare måste få rätt; de andra två, att öppna formuläret korrekt och spara ifyllda värden så att de faktiskt syns, är där de flesta överraskningarna gömmer sig, så alla tre täcks nedan

Att öppna ett formulär: FormFill, FormType och XFA-frågan

Fältåtkomst kräver att formulärifyllningssystemet, styrt av egenskapen FormFill, aktiveras innan dokumentet öppnas. Väl aktiv talar FormType om vilken sorts formulär du står inför, och svaret ändrar funktionsuppsättningen du kan utlova:

Diagram över FormFill-uppsättning och FormType-detekteringsgrenar i en Delphi PDFium Component-visare, som delar hanteringen i ftNone, ftAcroForm och ftXfaFull
FormType grenar så snart FormFill är aktiverad, och varje gren lovar en annan funktionsuppsättning
Pdf.FileName := FormPath;
Pdf.FormFill := True;   // aktivera före Active; krävs för all fältåtkomst
Pdf.Active := True;

case Pdf.FormType of
  ftNone:
    DisableFormPanel('This document has no interactive form');
  ftAcroForm:
    BuildFieldList;     // full fältnavigering och redigering tillgänglig
  ftXfaFull:
    ShowXfaNotice;      // XFA renderar från sin egen XML-mall;
                        // behandla fältredigering som begränsad
end;

Två praktiska noteringar följer av den switchen. AcroForm är standardformulärmodellen enligt ISO 32000, och det är vad varje API här riktar sig mot. XFA-dokument bäddar in sin egen XML-formulärarkitektur, så att lova en kund full XFA-redigering efter en snabb AcroForm-demo är ett åtagande du kommer att ångra. Den andra noteringen handlar om sidoeffekter: att sätta FormFill till True initierar också dokumentets JavaScript. I en dataregistreringsvisare är det precis rätt, eftersom beräkningsskript är vad som håller en löpande summa aktuell medan någon skriver. I ett förhandsgranskningsfönster för filer av okänt ursprung är det precis fel. Artikeln om säker PDF-förhandsgranskning täcker FormFill := False-sidan av den avvägningen

Tab-tangentnavigering som hamnar där användare förväntar sig

Tillbaka till tangentbordsproblemet från början. Frestelsen är att fejka Tab genom att syntetisera ett musklick på nästa widgets rektangel, vilket går sönder i samma ögonblick ett fält rullas utanför skärmen eller två widgetar överlappar. Fokus-API:et flyttar i stället formulärets eget fokus direkt, utan geometrigissningar. Fem anrop täcker det: FocusFormField efter index, FocusNextFormField och FocusPreviousFormField för stegning, FocusedFormFieldIndex för att läsa var du är, och ClearFormFieldFocus för att släppa fokus helt

Diagram över tabbtangentens fokusvandring i en Delphi PDFium Component-visare där FocusNextFormField wrapar inom ett blads tabbordning, och fem fokus-API:er täcker tangentbordsnavigering
Genomgången loopar inom en sidas tabbordning, så att korsa till nästa sida förblir visarens uppgift
procedure TFormViewer.HandleTabKey(Shift: TShiftState);
begin
  if ssShift in Shift then
    PdfView.FocusPreviousFormField
  else
    PdfView.FocusNextFormField;
  UpdateFieldStatus;  // t.ex. "Fält 4 av 17: InvoiceDate"
end;

Den enda beteendedetaljen som snubblar folk är omslaget. Navigeringen arbetar genom aktuell sidas tabbordning och loopar inom den: stega förbi det sista fältet och du är tillbaka vid det första. Båda stegningsfunktionerna returnerar det nya fältindexet, eller -1 när sidan inte har några fält alls. Den loopningen är per sida, inte per dokument, vilket betyder att korsa till nästa sida är ditt jobb, inte bibliotekets. Jämför det returnerade indexet mot det du började med, märk när det har slagit runt, och avancera PageNumber själv om formuläret är tänkt att läsas som en enda sammanhängande sekvens. Hoppa över den kontrollen och ett tvåsidigt formulär fångar tyst markören på sida ett, vilket är sin egen variant av det trasiga Tab-klagomålet

Navigeringen blir användbar när resten av gränssnittet reagerar på den. Händelsen OnFormFieldEnter utlöses när fokus anländer, och på visaren rapporterar OnFormFieldFocusChange det nya fältindexet, så att en sidopanel kan hålla jämna steg med vad tangentbordet just valde. När du behöver den omvända mappningen, från en skärmposition till ett fält, gör den indexerade egenskapen FormFieldAt träffprövningen för verktygstipsförhandsgranskningar och klicka-för-att-redigera-paneler. Det finns en tyst tillgänglighetsvinst i allt det här: eftersom fokus följer dokumentets egen fältordning är vägen du kopplar in för Tab-tangenten samma väg en skärmläsare tillkännager, utan extra arbete

Att visa fältnamn i stället för råa indexnummer tar en egenskap till. FormFieldInfo[] returnerar en TPdfFormFieldInfo-post per index, som bär fältnamnet, typen, teckenstorleken, det ikryssade tillståndet, exportvärdet och gruppmedlemskapet, vilket är vad en navigeringslista ska visa ("Fält 4 av 17: InvoiceDate" snarare än "4"). Radiogrupper är fallet värt en dedikerad testfil. Flera widgetar kan dela ett enda fältnamn, så en lista naivt sammansatt från widgetar visar samma grupp flera gånger och förvirrar alla som läser den

Varför ifyllda värden kommer ut tomma, och anropet som fixar det

Den andra klagomålstypen som fyller supportköer är mer alarmerande än en Tab-tangent som beter sig illa: ett formulär fylls i programmatiskt, kunden öppnar det i Acrobat, och varje fält ser tomt ut. Klicka in i ett fält och dess värde knäpps fram. Datan finns i filen hela tiden. Det som saknas är bilden av datan, och skälet är värt att förstå en gång eftersom det förklarar en hel familj av buggar

Ett AcroForm-textfält lagrar sitt värde i fältordbokens /V-post (ISO 32000-1 §12.7.3.3). Vad en visare faktiskt målar är något separat: widgetens utseendeström under /AP (§12.5.5), ett litet förrenderat innehållsutdrag. Skriv /V och lämna /AP orört, och de två glider isär. Värdet finns där; den renderade versionen av det är föråldrad eller frånvarande. Acrobat råkar bygga om ett fälts utseende när det får fokus, vilket är hela förklaringen till värden som bara syns vid klick. Den gamla NeedAppearances-flaggan, som bad visare att regenerera utseenden åt dig, fungerade aldrig enhetligt och är utfasad i PDF 2.0, och utskriftsservrar och miniatyrbildsgeneratorer ignorerar den helt. De målar /AP och inget annat, så om /AP är tomt skriver de ut en tom ruta

Att tilldela ett värde via FormField[i] skriver bara /V. Det är därför att fylla i ett formulär är en tresteg-sekvens, och steget team hoppar över är det mellersta:

Diagram över /V-värdet kontra /AP-utseendedrift i AcroForm-fält, och den tre stegs Delphi-fyllnadssekvensen byggd runt GenerateFormAppearances
Att tilldela värden skriver endast /V, och mellansteget är det som målar om det som utskriftsservrar faktiskt renderar
procedure TFormViewer.FillAndSave(const Values: array of WString;
  const OutputPath: string);
var
  i: Integer;
begin
  for i := 0 to Pdf.FormFieldCount - 1 do
    Pdf.FormField[i] := Values[i];   // skriver bara /V

  // Bygg om /AP-utseendeströmmarna; utan det här ser
  // formuläret tomt ut i Acrobat tills varje fält klickas
  Pdf.GenerateFormAppearances;

  Pdf.SaveAs(OutputPath);
end;

GenerateFormAppearances är hela fixen. Den bygger om varje widgets utseendeström från de aktuella värdena, typsnitten och quaddingen, så att en visare som aldrig kör en fokushändelse, en utskriftsserver eller en miniatyrgenerator, ändå målar det ifyllda tillståndet. Anropa den en gång efter batchen av tilldelningar, inte en gång per fält. Utseendegenerering gör verkligt layoutarbete, och anrop per fält multiplicerar det över ett stort formulär i onödan

Att regenerera utseenden är också ögonblicket då typsnitt och justering hävdar sig, vilket är källan till en andra ordningens överraskning. Den nya strömmen lägger ut varje värde inuti widgetrektangeln med hjälp av fältets typsnitt, storlek och quadding. Ett värde som sitter bekvämt i ditt testformulär kan klippas eller krympa i en kunds kopia där samma fält är smalare. Fält med automatisk storlek (teckenstorlek noll) krymper texten så att den passar; fält med fast storlek klipper den bara. Båda är lagliga, och det enda ärliga sättet att veta vilken ett givet formulär gör är att titta på den regenererade utdatan snarare än strängen du skrev. När någon rapporterar text avklippt vid kanten av en ruta är det här nästan alltid skälet

Behandla verifiering som en del av att avsluta arbetet, inte en eftertanke. Öppna den sparade filen i Acrobat och bekräfta att värdena syns innan du rör något fält. Skriv sedan ut den till PDF eller till en bild från en annan visare, en som ignorerar formulärlogik helt, och bekräfta att värdena överlever den vägen också. Tillsammans fångar de två kontrollerna varje variant av /V-mot-/AP-glidningen

Fältkonfigurationer som klarar demot och misslyckas i fält

Rena demoformulär döljer en uppsättning specialfall som kundfiler inte gör. Fyra av dem står för de flesta "det fungerade på min maskin"-rapporterna

  • Kryssrutans exportvärden. "På"-tillståndet är inte alltid Yes. Ett formulär är fritt att definiera sitt eget exportvärde, och att skriva fel sträng lämnar rutan visuellt okryssad medan din kod är övertygad om att den satte den. Läs exportvärdet från FormFieldInfo[] i stället för att anta ett
  • Radiogrupper med delat namn. Ett fält, flera widgetar. Värdet du tilldelar avgör vilken widget läses som vald, så gränssnittskod som antar att ett namn mappar till en rektangel hamnar med att rita fokusringen på fel knapp
  • Beräknade fält. Totalsummor underhållna av dokumentets JavaScript uppdateras som svar på fälthändelser. En programmatisk ifyllning som kringgår de händelserna måste antingen utlösa omberäkning eller skriva över de beräknade fälten direkt. Ett formulär där radposterna och totalsumman är oense är värre än endera fixen
  • Dolda obligatoriska fält. Villkorliga formulär döljer fält som fortfarande är flaggade som obligatoriska. Bestäm i förväg om din validering respekterar synlighet eller den råa obligatorisk-flaggan, skriv sedan ner det beslutet någonstans support kan hitta det

En distinktion är värd att reda ut innan den biter dig: att generera utseenden är inte utplattning. GenerateFormAppearances gör värden synliga överallt medan fälten förblir redigerbara. Utplattning bränner in utseendet i statiskt sidinnehåll och tar bort interaktiviteten för gott, vilket är rätt för en arkivkopia och fel för ett formulär nästa person fortfarande måste fylla i. Om FormType rapporterar ftXfaFull snarare än ftAcroForm, gäller ändå ingenting av redigeringsytan här rent, eftersom dokumentet renderas från sin egen XML-mall; upptäck det fallet och berätta för användaren, i stället för att låta dem hitta gränsen på egen hand

Formulärifyllningssystemet, fokusnavigeringen och utseendegenereringen som visas här är en del av PDFium Component för Delphi, C++Builder och Lazarus/FPC. Om din visare också hanterar granskarmarkeringar vid sidan av formulärdata, täcker artikeln om granskning av annoteringar den angränsande modellen