Tehnički članak

Izrada AcroForm polja i akcija u Delphiju uz HotPDF

AcroForm akcija je rječnik (dictionary) prikačen na widget koji govori pregledniku što učiniti kada se nešto dogodi tom widgetu. Kliknite na gumb i preglednik čita njegov rječnik akcije: URI akcija otvara web adresu, JavaScript akcija pokreće skriptu, SubmitForm akcija šalje prikupljene vrijednosti polja na krajnju točku (endpoint), ResetForm akcija čisti ih natrag na zadane postavke. Akcija su podaci, a ne ponašanje ugrađeno u datoteku. ISO 32000-1 §12.6 definira oblik rječnika; preglednik isporučuje mehanizam koji ga interpretira. Ta podjela je bitna zato što akcija koja je savršeno zapisana u PDF i dalje ne radi ništa ako čitač (reader) na drugoj strani nema mehanizam za nju, i velik dio AcroForm tuge vuče korijene iz te praznine umjesto iz loše oblikovanog polja

HotPDF zapisuje te rječnike izravno iz Delphija i C++Buildera, uz widgete polja o kojima vise. Dvije strukture su u igri za svaki interaktivni obrazac: widget koji korisnik vidi na stranici, i polje zajedno sa mehanizmom akcije ispod koji nosi podatke i ožičenje. Oni se uređuju neovisno, i bilo koje od to dvoje može biti pogrešno dok ono drugo izgleda u redu. Odjeljci u nastavku prolaze kroz imenovanje polja, same akcije gumba, JavaScript na razini polja, i klasu defekta koja preživi vizualnu provjeru jer živi u potpunosti u drugoj strukturi

Imena polja su ključevi za usmjeravanje, a ne natpisi

Svako AcroForm polje nosi potpuno kvalificirano ime. ISO 32000-1 §12.7.3 čini to ime, a ne vidljivi natpis, ključem pod kojim vrijednost polja putuje kada je obrazac izvezen ili poslan. Programeri koji dolaze iz VCL dizajna skloni su tretirati ime kontrole kao privatni identifikator koda, a to ovdje nije. To je "wire format" (format prijenosa)

Prva stvar koja iz toga slijedi je da dva polja sa istim potpuno kvalificiranim imenom zapravo nisu dva polja. PDF ih tretira kao dvije anotacije widgeta jednog te istog polja, koje dijele jednu vrijednost, tako da tipkanje u jedno odmah ažurira i ono drugo. To je točno ono što želite kada se ime klijenta mora ponoviti na svakoj stranici ugovora. To je međutim bug kada petlja za generiranje slučajno iznova koristi 'Field1' preko tri stranice. Nijedna vizualna inspekcija ne hvata taj drugi slučaj. Svaka stranica i dalje crta svoj vlastiti okvir, a ta se veza pojavi tek kada netko počne tipkati

Imena sa točkom kao što je applicant.email grade hijerarhiju. Roditeljski čvor applicant grupira svoju djecu, što je ono što omogućava da resetiranje ili slanje cilja samo dio obrasca. Imenovanje polja na ovaj način od samog početka ne košta ništa, i isplati se onaj prvi put kada sustav primatelj zatraži samo taj aplikantski blok

Radio gumbi imaju svoje vlastito pravilo. Gumbi koji bi se trebali mijenjati zajedno moraju dijeliti ime grupe. U HotPDF-u, AddRadioButton pozivi koji prosljeđuju isto ime grupe prikače svoje widgete na jedno roditeljsko polje, a izvozna vrijednost svakog gumba ('basic' ili 'full') identificira odabranu opciju. Dajte svakom gumbu različito ime i dobit ćete niz neovisnih on/off prekidača umjesto jedne međusobno isključive grupe, što se renderira identično a ponaša pogrešno

Stvaranje skupa polja stranicu po stranicu

HotPDF postavlja polja kroz metode THPDFPage, tako da svako polje pripada onom objektu stranice koji ga je stvorio. Zamka sekvenciranja na koju treba paziti je AddPage. Ona preusmjerava CurrentPage na novu stranicu u onom trenutku u kojem se vrati, tako da bilo koji poziv polja nakon nje sleti na tu novu stranicu čak i kada je polje logički pripadalo stranici koju ste upravo napustili. Završite svaku stranicu, nacrtani sadržaj i polja skupa, prije nego pozovete AddPage

procedure BuildClaimForm(Pdf: THotPDF);
begin
  // Page 1: applicant block
  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 now points at page 2
  Pdf.CurrentPage.AddListBox('riders', 'None',
    ['None', 'Flood', 'Earthquake'], Rect(50, 500, 200, 600));
end;

Koordinate koriste PDF konvenciju, s izvorištem u donjem lijevom kutu stranice. To je isto ono izvorište koje TextOut koristi za iscrtani tekst, tako da Rect(50, 100, 200, 120) sjedi blizu dna "Letter" stranice, a ne blizu vrha. VCL stavlja Y na vrh i raste prema dolje, stoga tabela rasporeda prenesena izravno izađe okomito zrcaljena, sa svakim poljem preokrenutim na pogrešan kraj stranice. Napravite pretvorbu jednom u zajedničkom "helperu" umjesto na svakom mjestu poziva, i ta jedna ispravka rješava čitav obrazac

Povezivanje gumba na URI, JavaScript, i submit akcije

"Push" gumb je inertan sve dok mu nije priključena akcija. HotPDF izlaže vrste akcija iz ISO 32000-1 §12.6.4 kroz THPDFButtonAction enumeraciju (baURI, baJavaScript, baSubmitURL, baResetForm, baHide, baShow, baNamed), te pruža dvije metode koje stvaraju gumb i vežu njegovu akciju u jednom pozivu

// Open a help page in the system browser
Pdf.CurrentPage.AddPushButtonWithAction('btnHelp', 'Help',
  'https://www.example.com/claims-help', Rect(320, 700, 420, 730), baURI);

// Run viewer-side JavaScript
Pdf.CurrentPage.AddPushButtonWithAction('btnRecalc', 'Recalculate',
  'app.alert("Totals updated.");', Rect(320, 660, 420, 690), baJavaScript);

// Submit as XFDF and keep empty fields in the payload
Pdf.CurrentPage.AddPushButtonWithSubmitAction('btnSubmit', 'Submit claim',
  'https://api.example.com/claims', Rect(320, 620, 420, 650),
  [sffXFDF, sffIncludeNoValueFields]);

Zastavice slanja (submit flags) zaslužuju više razmišljanja nego što ga inače dobivaju. AddPushButtonWithSubmitAction uzima skup THPDFSubmitFormFlags, a prazan skup proizvodi običan url-kodirani post, što je format koji mnoge primjerne krajnje točke prihvaćaju a mnoge produkcijske krajnje točke odbacuju. Dodavanje sffXFDF prebacuje "payload" u XFDF. sffGetMethod mijenja HTTP glagol. sffIncludeNoValueFields zadržava prazna polja u payloadu umjesto da ih tiho ispusti, što postaje bitno u trenutku kada potrošač razlikuje pojam "odsutno" od pojma "prazno". Taj skup zastavica je dio vašeg ugovora o sučelju sa primajućom krajnjom točkom, stoga to riješite u dogovoru sa timom koji parsira primljene podatke, a ne tek nakon prve odbijene serije

JavaScript na razini polja: keystroke (pritisak tipke), format, validate (provjera)

Klikovi na gumbe nisu jedino mjesto gdje žive akcije. HotPDF također prikačuje JavaScript na događaje po svakom polju koje preglednici sposobni za izvođenje skripti ispaljuju dok korisnik unosi podatke. Postoje tri okidača, i oni opaljuju u različitim točkama u životnom ciklusu unosa. Keystroke akcija pokreće se kako pristiže svaki pojedini znak, te opet pri konačnom unosu (commit). Format akcija prepisuje prikazanu vrijednost nakon što je promjena unesena, i to čisto radi same prezentacije. Validate akcija pak ima zadnju riječ, prihvaćajući ili odbijajući taj konačni unos prije nego on postane službena vrijednost samog polja

// Reject committed values that are not plausible email addresses
Pdf.AttachFieldKeyStrokeAction('applicant.email',
  'if (event.willCommit && !/^[\w.-]+@[\w.-]+\.\w+$/.test(event.value)) event.rc = false;');

// Display US phone numbers as (NNN) NNN-NNNN
Pdf.AttachFieldFormatAction('applicant.phone',
  'event.value = event.value.replace(/(\d{3})(\d{3})(\d{4})/, "($1) $2-$3");');

// Refuse applicants under 18 at commit time
Pdf.AttachFieldValidateAction('applicant.age',
  'if (parseInt(event.value) < 18) event.rc = false;');
$2-$3");'); // Refuse applicants under 18 at commit time Pdf.AttachFieldValidateAction('applicant.age', 'if (parseInt(event.value) < 18) event.rc = false;');

Postavljanje event.rc = false unutar keystroke ili validate skripte govori pregledniku da odbije taj unos. Caka je u tome da se ništa od ovoga ne pokreće osim ako preglednik ne isporučuje u sebi nekakav JavaScript mehanizam. Acrobat i nekoliko desktop proizvoda ga imaju. Većina mobilnih čitača, u preglednik ugrađenih renderera i onih za "print pipelines" ga pak nema, te oni te skripte prosto ispuste bez pritužbi. Tako te skripte na poljima poboljšavaju kvalitetu podataka samo za onaj podskup korisnika čiji ih čitač doista izvede, i to je to. One nisu sigurnosna granica. Svaka poslana vrijednost i dalje mora biti validirana na samom serveru nakon što pristigne, jer ne možete nikada pretpostaviti da je klijent sam išta provjerio

Defekti koji prolaze vizualnu provjeru

Najteži AcroForm defekti za uhvatiti su oni koji žive u podatkovnoj strukturi radije nego u renderiranju, zato što otvaranje datoteke i gledanje iste vam ne govori apsolutno ništa. Četiri takva se pojavljuju dovoljno često da ih vrijedi imenovati, a svaki ima mehanički test koji ih pronalazi prije izdanja

  • Odstupanje izvozne vrijednosti (Export value drift). Potvrdni okvir stvoren kao AddCheckBox('consent', 'Yes', ...) poslat će Yes. Potrošač koji podudara to sa Y odbija svaki takav unos dok ta stranica i dalje izgleda savršeno. Ispunite obrazac, izvezite ga kao XFDF iz Acrobata, i usporedite te vrijednosti prema shemi koju potrošač zaista i očekuje
  • Slučajno zrcaljenje vrijednosti (Accidental value mirroring). Dva polja koja dijele jedno te isto potpuno kvalificirano ime spajaju se u jedno. Taj simptom se pojavi tek u trenutku unosa podataka i to baš nikada za vrijeme generiranja, tako da se test tu sastoji u tipkanju u obrazac, a ne u pukom renderiranju pa da to onda odmjeravate od oka
  • Combo vrijednosti izvan popisa opcija. Kada trenutna vrijednost proslijeđena u AddComboBox nije unutar nabrojanih opcija, preglednici se ne slažu oko toga trebaju li to uopće prikazati, ostaviti to prazno, ili to nekako označiti. Zadržite tu zadanu vrijednost unutar popisa i to neslaganje jednostavno nestaje
  • Polja koja su i dalje uređiva nakon što je tijek posla zatvoren. HotPDF zapravo i nema poziv za izravnavanje izgleda za AcroForm polja. Ovdje podržani način zamrzavanja već dovršenog obrasca jest samo stvoriti ta polja uz ffReadOnly zastavicu, koja zadržava vidljivu vrijednost kroz vlastiti tok samog izgleda polja dok naprosto odbija nekakva nova uređivanja. Polje pak i dalje ostaje objekt takozvanog "živog" obrasca, što je ono što će nizvodni alati za sastavljanje pa onda i digitalno potpisivanje uopće i očekivati

Jedno takvo ponašanje na strani preglednika je itekako vrijedno napomene oko regresije iako zapravo niti jedna promjena tu u kodu isto ne rješava. Poduzetnička uvođenja Acrobata mogu naprosto onesposobiti JavaScript ili baš restriktivno ograničiti odredišta putem nekakve police ponašanja, stoga jedna te ista takva akcija koja je radila posve kroz svaku razvojnu verziju itekako može onda samo "sjediti mrtva" na nekakvom tamo zaključanom klijentskom korisničkom sučelju. Planirajte zato obavezno jedan takav vidljivi rezervni plan za slučajeve kada onaj famozni gumb ne napravi ništa, pa makar i taj navedeni rezervni samo u naravi bio neka ispisana instrukcija po kojoj vi samom tom korisniku objašnjavate što on umjesto toga onda zapravo treba učiniti

Gdje se rad na obrascu spaja sa ostatkom dokumenta

Polje s potpisom i samo je usput vrsta AcroForm polja. Obrazac koji će zapravo onda kasnije biti certificiran ili supotpisan proći će bolje rezervirajući to polje tijekom generiranja nego da se zakrpa naknadno, a razlozi na razini bajtova zašto su u popratnom članku o digitalnim potpisima i PAdES potpisivanju uz HotPDF. Unosi koji dolaze kao XFA paketi umjesto izvornih AcroForma su drugačija situacija: izravnavanje XFA-e u AcroForm polja je njegov vlastiti tijek posla s vlastitim modelom gubitka, zato što te dvije tehnologije obrazaca ne mogu koegzistirati u jednoj datoteci

Pozivi AddPushButtonWithSubmitAction, AddRadioButton, i AttachFieldValidateAction prikazani ovdje dio su komponente HotPDF Component za Delphi i C++Builder; stranica proizvoda povezuje cijelu referencu uključujući i zastavice za polja preopterećenja i potpunu enumeraciju zastavica za slanje