Teknisk artikel

HotPDF Delphi Component: AcroForm fields and action logic in Delphi

En AcroForm-åtgärd är en ordbok fäst vid en widget som talar om för visaren vad den ska göra när något händer med den widgeten. Klicka på en knapp och visaren läser dess åtgärdsordbok: en URI-åtgärd öppnar en webbadress, en JavaScript-åtgärd kör skript, en SubmitForm-åtgärd postar de insamlade fältvärdena till en slutpunkt, en ResetForm-åtgärd rensar dem tillbaka till standardvärden. Åtgärden är data, inte beteende inbakat i filen. ISO 32000-1 §12.6 definierar ordbokens form; visaren tillhandahåller motorn som tolkar den. Den uppdelningen spelar roll eftersom en åtgärd skriven perfekt in i PDF:en ändå inte gör någonting om läsaren i andra änden saknar en motor för den, och mycket av AcroForm-eländet spårar tillbaka till den klyftan snarare än till ett felformat fält

HotPDF skriver de ordböckerna direkt från Delphi och C++Builder, jämte fältwidgetarna de hänger på. Två strukturer är i spel för varje interaktivt formulär: widgeten användaren ser på sidan, och fält- plus åtgärdsmaskineriet därunder som bär datan och kopplingen. De redigeras oberoende av varandra, och endera kan vara fel medan den andra ser bra ut. Avsnitten nedan går igenom fältnamngivning, själva knappåtgärderna, JavaScript på fältnivå, och den defektklass som överlever en visuell kontroll eftersom den lever helt i den andra strukturen

AcroForm-widgetlagret i HotPDF mappat till de underliggande fältvärdena, submit-åtgärdsordboken och ett missmatchat exportvärde för samtycke
Användare klickar på widgetlagret medan värden färdas genom fält- och åtgärdslagret under, där avvikelser förblir osynliga

Fältnamn är dirigeringsnycklar, inte bildtexter

Varje AcroForm-fält bär ett fullt kvalificerat namn. ISO 32000-1 §12.7.3 gör det namnet, inte den synliga bildtexten, till nyckeln under vilken fältets värde färdas när formuläret exporteras eller skickas in. Utvecklare som kommer från VCL-design brukar behandla en kontrolls namn som en privat kodidentifierare, och det är det inte här. Det är trådformatet

Det första som följer är att två fält med samma fullt kvalificerade namn inte är två fält. PDF behandlar dem som två widgetanoteringar av ett fält, som delar ett värde, så att skriva i det ena uppdaterar det andra på fläcken. Det är precis vad du vill ha när ett kundnamn måste upprepas på varje sida i ett avtal. Det är en bugg när en genereringsloop återanvänder 'Field1' över tre sidor av misstag. Ingen visuell inspektion fångar det andra fallet. Varje sida ritar fortfarande sin egen ruta, och kopplingen syns bara när någon börjar skriva

Namn med punkter som applicant.email bygger en hierarki. Föräldranoden applicant grupperar sina barn, vilket är det som låter en återställning eller inlämning rikta sig mot bara en del av ett formulär. Att namnge fält på det här sättet från början kostar ingenting, och det betalar sig själv första gången det mottagande systemet ber om bara sökandeblocket

Radioknappar har en egen regel. Knappar som ska växla tillsammans måste dela ett gruppnamn. I HotPDF fäster AddRadioButton-anrop som skickar samma gruppnamn sina widgetar vid ett gemensamt föräldrafält, och varje knapps exportvärde ('basic' eller 'full') identifierar det valda alternativet. Ge varje knapp ett distinkt namn och du får en rad oberoende på/av-brytare i stället för en ömsesidigt uteslutande grupp, vilket återges identiskt men beter sig fel

Att skapa fältuppsättningen sida för sida

HotPDF placerar fält via THPDFPage-metoder, så varje fält tillhör sidobjektet som skapade det. Sekvenseringsfällan att hålla utkik efter är AddPage. Den pekar om CurrentPage mot den nya sidan i samma ögonblick den returnerar, så alla fältanrop efter den hamnar på den nya sidan även när fältet logiskt tillhörde sidan du precis lämnade. Slutför varje sida, ritat innehåll och fält tillsammans, innan du anropar AddPage

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Sida 1: sökandeblocket
  Pdf.CurrentPage.AddTextField('applicant.name', '', Rect(50, 700, 300, 722));
  Pdf.CurrentPage.AddTextField('applicant.email', '', Rect(50, 660, 300, 682));
  Pdf.CurrentPage.AddCheckBox('consent', 'Y', Rect(50, 620, 70, 640), False);
  Pdf.CurrentPage.AddRadioButton('coverage', 'basic', Rect(50, 580, 70, 600), True);
  Pdf.CurrentPage.AddRadioButton('coverage', 'full', Rect(90, 580, 110, 600), False);
  Pdf.CurrentPage.AddComboBox('plan', 'Standard',
    ['Basic', 'Standard', 'Premium'], Rect(50, 540, 200, 565));

  Pdf.AddPage;  // CurrentPage pekar nu mot sida 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;

Koordinater använder PDF-konventionen, med origo i sidans nedre vänstra hörn. Det är samma origo TextOut använder för ritad text, så Rect(50, 100, 200, 120) sitter nära botten av en Letter-sida, inte toppen. VCL lägger Y högst upp och låter det växa nedåt, så en layouttabell portad rakt över kommer ut vertikalt speglad, varje fält vänt till fel ände av sidan. Gör konverteringen en gång i en delad hjälpfunktion i stället för vid varje anropsplats, och en enda korrigering fixar hela formuläret

Att koppla knappar till URI-, JavaScript- och inlämningsåtgärder

En tryckknapp är inert tills en åtgärd fästs vid den. HotPDF exponerar åtgärdstyperna från ISO 32000-1 §12.6.4 via uppräkningen THPDFButtonAction (baURI, baJavaScript, baSubmitURL, baResetForm, baHide, baShow, baNamed), och tillhandahåller två metoder som skapar knappen och binder dess åtgärd i ett enda anrop

HotPDF-knappåtgärdstyper i Delphi: baURI-länkar, baJavaScript-skript, och SubmitForm-postning med explicita formatflaggor
Ett enda bindningsanrop fäster vilken som helst av de tre åtgärdsordlistorna, och endast submit-varianten bär ett flaggavtal med den mottagande ändpunkten
// Öppna en hjälpsida i systemets webbläsare
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);

// Kör JavaScript på visarsidan
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);

// Skicka som XFDF och behåll tomma fält i nyttolasten
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 620, 420, 650),
  [sffXFDF, sffIncludeNoValueFields]);

Inlämningsflaggorna förtjänar mer eftertanke än de vanligtvis får. AddPushButtonWithSubmitAction tar en THPDFSubmitFormFlags-mängd, och en tom mängd producerar en vanlig url-kodad post, vilket är formatet många exempel-slutpunkter accepterar och många produktionsslutpunkter avvisar. Att lägga till sffXFDF växlar nyttolasten till XFDF. sffGetMethod ändrar HTTP-verbet. sffIncludeNoValueFields behåller tomma fält i nyttolasten i stället för att tyst släppa dem, vilket spelar roll i samma ögonblick konsumenten skiljer "frånvarande" från "tomt". Flaggmängden är en del av ditt gränssnittskontrakt med den mottagande slutpunkten, så bestäm den tillsammans med teamet som parsar inlämningen, inte efter den första avvisade batchen

JavaScript på fältnivå: tangenttryckning, format, validering

Knappklick är inte den enda platsen åtgärder lever på. HotPDF fäster också JavaScript vid de per-fält-händelser som skriptkapabla visare utlöser medan en användare matar in data. Det finns tre utlösare, och de utlöses vid olika punkter i inmatningens livscykel. En tangenttryckningsåtgärd körs allteftersom varje tecken anländer, och igen vid verkställande. En formatåtgärd skriver om det visade värdet efter att en ändring verkställts, rent för presentation. En valideringsåtgärd får sista ordet, och accepterar eller vägrar det verkställda värdet innan det blir fältets värde

Livscykeln för HotPDF:s fältnivå-JavaScript-händelser från tangenttryckning till validate till format, med varningen om validering på serversidan nedanför
Keystroke- och validate-skript kan avvisa indata medan format bara retuscherar visningen, och inget skript överlever en läsare utan JavaScript-motor
// Avvisa verkställda värden som inte är rimliga e-postadresser
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Visa amerikanska telefonnummer som (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Vägra sökande under 18 vid verkställandetillfället
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');

Att sätta event.rc = false inuti ett tangenttryckning- eller valideringsskript talar om för visaren att avvisa indatan. Haken är att inget av det här körs om inte visaren levereras med en JavaScript-motor. Acrobat och ett fåtal skrivbordsprodukter har en. De flesta mobila läsare, webbläsarinbäddade renderare och utskriftspipelines har det inte, och de släpper skripten på golvet utan protest. Så fältskript förbättrar datakvaliteten för den delmängd användare vars läsare kör dem, och det är allt de gör. De är ingen säkerhetsgräns. Varje inskickat värde måste fortfarande valideras på servern när det anländer, eftersom du inte kan anta att klienten kontrollerade något

Defekter som klarar visuell granskning

De svåraste AcroForm-defekterna att fånga är de som lever i datastrukturen snarare än renderingen, eftersom att öppna filen och titta på den inte säger dig någonting. Fyra dyker upp tillräckligt ofta för att vara värda att namnge, och var och en har ett mekaniskt test som hittar den före release

  • Exportvärdesglidning. En kryssruta skapad som AddCheckBox('consent', 'Yes', ...) postar Yes. En konsument som matchar mot Y avvisar varje inlämning medan sidan ser perfekt ut. Fyll i formuläret, exportera det som XFDF från Acrobat, och diffa värdena mot schemat konsumenten faktiskt förväntar sig
  • Oavsiktlig värdespegling. Två fält som delar ett fullt kvalificerat namn slås ihop till ett. Symptomet visar sig vid datainmatningstillfället och aldrig vid genereringstillfället, så testet är att skriva i formuläret, inte att rendera det och okulärbesiktiga resultatet
  • Kombinationsvärden utanför alternativlistan. När det aktuella värdet som skickas till AddComboBox inte är ett av de listade alternativen, är visare oense om huruvida de ska visa det, tömma det, eller flagga det. Håll standardvärdet inom listan så försvinner oenigheten
  • Fält fortfarande redigerbara efter att arbetsflödet stängts. HotPDF har inget utseende-utplattningsanrop för AcroForm-fält. Det stödda sättet att frysa ett färdigifyllt formulär är att skapa fälten med flaggan ffReadOnly, vilket håller värdet synligt genom fältets egen utseendeström samtidigt som redigeringar vägras. Fältet förblir ett levande formulärobjekt, vilket är vad nedströms sammanställnings- och signeringsverktyg förväntar sig att hitta

Ett beteende på visarsidan är värt en regressionsanteckning även om ingen kodändring adresserar det. Företagsdriftsättningar av Acrobat kan inaktivera JavaScript eller begränsa inlämningsmål via policy, så en åtgärd som fungerade genom varje utvecklingsbygge kan ligga död på ett låst kundskrivbord. Planera en synlig reservlösning för fallet där knappen inte gör någonting, även om den reservlösningen bara är en tryckt instruktion som talar om för användaren vad hen ska göra i stället

Var formulärarbetet kopplar till resten av dokumentet

Ett signaturfält är i sig en AcroForm-fälttyp. Ett formulär som senare ska certifieras eller kontrasigneras klarar sig bättre av att reservera det fältet under generering än att få det inplåstrat efteråt, och skälen på bytenivå finns i följdartikeln om digitala signaturer och PAdES-signering med HotPDF. Indata som anländer som XFA-paket snarare än nativt AcroForm är en annan situation: att platta ut XFA till AcroForm-fält är sitt eget arbetsflöde med sin egen förlustmodell, eftersom de två formulärteknologierna inte kan samexistera i en fil

Fält-, åtgärds- och utlösarmetoderna som visas här är en del av standard-API:et för HotPDF Delphi Component för Delphi och C++Builder; produktsidan länkar den fullständiga referensen, inklusive fältflagg-overloaden och den kompletta inlämningsflagg-uppräkningen