Un câmp de formular PDF în sine este doar o casetă care conține o valoare. Ceea ce face ca un formular să se comporte ca o mică aplicație este acțiunea atașată acestuia: un clic care ascunde o secțiune, preia valori salvate dintr-un fișier, sare la ultima pagină sau rulează un script care calculează totalul unei coloane. Nimic din toate acestea nu trăiește în câmp. Trăiește într-un dicționar de acțiuni, iar ISO 32000-1 organizează întreaga familie în §12.6. Acest articol prezintă acțiunile pe care un program Delphi le folosește cel mai des și arată cum PDFlibPas conectează fiecare acțiune la un câmp sau la un link.
Modelul mental care merită reținut este că un câmp și o acțiune sunt obiecte separate, unite printr-o referință. O adnotare de tip widget sau o adnotare de tip link poartă o acțiune în intrarea sa /A. Acțiunea numește câmpul pe care operează prin titlu, nu prin index, astfel încât titlul pe care îl dați unui câmp este identificatorul pe care fiecare acțiune ulterioară îl folosește pentru a-l găsi. Odată ce această separare este clară, API-ul nu mai arată ca o colecție aleatorie de apeluri, ci ca un singur model aplicat la patru tipuri de verbe.
Acțiuni denumite: navigare fără număr de pagină
Cele mai simple acțiuni nu poartă deloc parametri. ISO 32000-1 §12.6.4.11, Tabelul 194, definește acțiunile denumite: vizualizatorul interpretează un nume simbolic la rulare în loc să urmeze o destinație stocată. Patru nume sunt acceptate universal și sunt exact cele pe care un cititor le așteaptă de la o bară de instrumente: NextPage, PrevPage, FirstPage și LastPage. Deoarece destinația este relativă la orice pagină pe care o afișează vizualizatorul în acel moment, un buton Next creat în acest mod funcționează pe fiecare pagină fără a fi nevoie să calculați o țintă.
În PDFlibPas, o acțiune denumită este atașată unui dreptunghi hotspot pe pagina curentă. Al patrulea și al cincilea argument de tip întreg selectează verbul și aspectul.
// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Options bit 0 (value 1) draws a border around the hotspot
Pdf.AddLinkToNamedAction(500, 560, 60, 18, 0, 1); // Next
Pdf.AddLinkToNamedAction(40, 560, 60, 18, 1, 1); // Previous
Pdf.AddLinkToNamedAction(110, 560, 60, 18, 3, 1); // jump to last page
Nu există nicio destinație de menținut în sincronizare, ceea ce este și ideea. O acțiune denumită supraviețuiește inserării și ștergerii paginilor pentru că nu numește niciodată o pagină în primul rând. Comparați acest lucru cu un link go-to explicit, care stochează un index de pagină țintă pe care trebuie să îl renumerotați în momentul în care documentul crește.
Acțiunea Hide și problema legată de tablouri
Acțiunea Hide, ISO 32000-1 §12.6.4.10, Tabelul 196, comută vizibilitatea unuia sau mai multor câmpuri. Es cel mai simplu mod de a construi un comportament de afișare și ascundere fără scripting și este ceea ce doriți pentru un link Show details sau pentru două panouri reciproc exclusive în care afișarea unuia îl ascunde pe celălalt. Acțiunea poartă o țintă în intrarea sa /T și o valoare booleană /H care decide direcția: ascunde când este adevărat, afișează când este fals.
Subtilitatea constă în întregime în modul în care este codificată acea țintă și este genul de detaliu care produce un formular care funcționează pe mașina dumneavoastră și eșuează pe cea a clientului. Când acțiunea numește un singur câmp, /T este scris ca un singur șir de text. Când numește mai multe, /T este scris ca un tablou de șiruri de text. Vizualizatoarele mai vechi nu tratează un tablou cu un singur element în același mod în care tratează un șir simplu, așa că codificarea trebuie să se ramifice în funcție de număr: un singur nume trebuie emis ca un șir, nu ca un tablou cu lungimea unu, pentru ca cea mai largă gamă de cititoare să îl respecte. PDFlibPas ia această decizie pentru dumneavoastră. Transmiteți numele câmpurilor separate prin virgule, punct și virgulă sau întreruperi de rând, iar scriitorul emite un singur șir pentru un singur nume și un tablou pentru două sau mai multe.
// HideFlag non-zero hides the listed fields (/H true); zero shows them.
// One name -> /T is a text string. Two or more -> /T is an array of strings.
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
'ShippingName,ShippingAddress,ShippingZip', 1, 1);
Deoarece acțiunea nu face referire la nicio resursă externă, rămâne compatibilă cu PDF/A. Numele pe care le transmiteți sunt titluri de câmpuri complet calificate, motiv pentru care un câmp copil din interiorul unui grup trebuie adresat prin calea sa completă cu puncte, mai degrabă decât prin numele său simplu de frunză.
ImportData: precompletarea din FDF
Acolo unde acțiunea Hide rearanjează ceea ce este deja pe pagină, acțiunea import-data aduce valori din afara acesteia. ISO 32000-1 §12.6.4.8, Tabelul 198, o definește ca o acțiune care populează AcroForm dintr-un fișier Forms Data Format de pe disc. Aceasta este acțiunea din spatele unui control Reload sample data sau Reset to defaults, unde un fișier FDF este livrat alături de PDF și conține valorile canonice ale câmpurilor. Apelul le oglindește pe celelalte, preluând dreptunghiul hotspot, calea către FDF și o mască de biți pentru aspect: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Fișierul nu trebuie să existe atunci când este construit PDF-ul, dar trebuie să fie prezent atunci când utilizatorul face clic, iar orice backslash-uri din cale sunt rescrise automat în forma de slash canonică pentru PDF.
O constrângere merită menționată clar deoarece reprezintă o surpriză frecventă. O acțiune import-data indică un fișier extern, deci nu este permisă în PDF/A. Când documentul este în modul PDF/A, apelul returnează zero și nu adaugă nimic, în loc să producă un fișier care eșuează la validare. Dacă fluxul dumneavoastră vizează un rezultat de arhivare, precompletarea trebuie să aibă loc la momentul generării prin scrierea directă a valorilor câmpurilor, nu prin amânarea lor la un clic.
JavaScript: pachete globale și scripturi per acțiune
Pentru o logică ce depășește afișarea, ascunderea și importul, familia de acțiuni apelează la JavaScript la nivel de document. Există două locuri distincte în care poate trăi un script, iar diferența contează. Un pachet JavaScript la nivel de document este stocat o singură dată pentru întregul fișier și rulează când documentul se deschide, ceea ce îl face locul potrivit pentru definițiile de funcții și starea partajată. Un script per acțiune este atașat unui singur link sau câmp și rulează numai atunci când acel obiect este activat, ceea ce îl face locul potrivit pentru singura linie care apelează o funcție pe care pachetul a definit-o deja.
PDFlibPas le expune pe ambele. AddGlobalJavaScript stochează un pachet numit la nivel de document; reutilizarea unui nume înlocuiește tot ce a fost stocat sub el. AddLinkToJavaScript atașează un script unui hotspot, astfel încât un clic să îl execute.
// Document-level package: define a reusable function once.
Pdf.AddGlobalJavaScript('Totals',
'function recalcTotal() {' +
' var net = this.getField("Net").value;' +
' var tax = this.getField("Tax").value;' +
' this.getField("Gross").value = Number(net) + Number(tax);' +
'}');
// Per-action script on a link: just call the shared function.
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);
Păstrarea funcției în pachetul global și a apelului în link nu este o preferință de stil. Evită duplicarea aceluiași corp pe fiecare control care are nevoie de el și înseamnă că un vizualizator cu scripting-ul dezactivat pur și simplu nu face nimic la clic, în loc să se blokeze la un blob inline malformat. De asemenea, menține intrările per acțiune mici, ceea ce păstrează fișierul lizibil atunci când îl inspectați mai târziu.
Câmpuri, câmpuri copil și înghețarea rezultatului
Acțiunile au nevoie de câmpuri pe care să acționeze, așa că este util să vedem cum ia naștere un câmp. NewFormField creează un câmp pe pagina curentă și returnează indexul acestuia; tipul întreg selectează tipul de câmp, unde 1 este Text, 2 este Pushbutton, 3 este Checkbox, 4 este Radiobutton, 5 este Choice, 6 este Signature și 7 este un câmp Parent care deține copii, dar nu desenează nimic în sine. Titlul pe care îl transmiteți nu poate conține punct, deoarece punctul este separatorul în numele complet calificate pe care acțiunile le folosesc pentru a adresa copiii.
Grupurile radio și formularele ierarhice sunt construite oferind câmpului părinte copii. NewChildFormField adaugă un copil sub un părinte numit, iar pentru cazurile radio și de selecție, AddFormFieldSub adaugă opțiunile individuale și returnează un index temporar pe care îl folosiți pentru a le poziționa pe fiecare. Când faza interactivă s-a încheiat și doriți să înghețați un câmp astfel încât aspectul său curent să devină conținut permanent al paginii, FlattenFormField desenează câmpul pe pagină și îl elimină din formular. După o aplatizare, indicii câmpurilor ulterioare se deplasează în jos cu unu, ceea ce este singurul lucru de reținut dacă aplatizați mai multe câmpuri într-o buclă.
var
Pdf: TPDFlib;
FldShip: Integer;
begin
Pdf := TPDFlib.Create;
try
Pdf.SetOrigin(1); // top-left origin
Pdf.SetPageSize('A4');
Pdf.NewPage;
// A text field the Hide action will target by its title.
FldShip := Pdf.NewFormField('ShippingAddress', 1);
Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
Pdf.SetFormFieldValue(FldShip, '');
// Wire a Hide link and a navigation link to this page.
Pdf.DrawText(40, 110, 'Toggle shipping block:');
Pdf.AddLinkToHideField(220, 100, 70, 16, 'ShippingAddress', 1, 1);
Pdf.AddLinkToNamedAction(500, 800, 60, 18, 3, 1); // Last page
// A document-level script available to every event in the file.
Pdf.AddGlobalJavaScript('OnOpen',
'app.alert("Form ready", 3);');
// Freeze the field if the output should no longer be editable.
// Pdf.FlattenFormField(FldShip);
if Pdf.SaveToFile('form_actions.pdf') <> 1 then
raise Exception.Create('Save failed');
finally
Pdf.Free;
end;
end;
Apelul flatten este comentat intenționat. Lăsați-l deoparte și documentul va fi livrat ca un formular activ ale cărui acțiuni se declanșează în cititor. Activați-l și câmpul este redat sub formă de marcaje statice, ceea ce doriți atunci când formularul a fost completat iar rezultatul ar trebui să circule ca o înregistrare fixă. Același câmp, același cod, două documente foarte diferite în funcție de faptul dacă îl înghețați sau nu.
Alegerea verbului potrivit
Cele patru acțiuni se împart clar în funcție de ceea ce ating. O acțiune denumită mută fereastra de vizualizare și nu are nevoie de niciun câmp. O acțiune Hide modifică vizibilitatea și are nevoie de titlurile câmpurilor, codificarea de tip șir versus tablou fiind gestionată automat pentru dumneavoastră. O acțiune import-data accesează un fișier de pe disc și, prin urmare, este interzisă în PDF/A. O acțiune JavaScript rulează o logică arbitrară și este cel mai bine să fie împărțită între un pachet global de funcții și apeluri mici per acțiune. Optați pentru cea mai simplă acțiune care își face treaba: o acțiune Hide este mai portabilă decât un script care setează un flag ascuns, iar o acțiune denumită este mai durabilă decât o destinație de pagină stocată, deoarece nu există un număr de întreținut.
De aici, două subiecte adiacente completează imaginea. Dacă formularul face parte dintr-un document accesibil, arborele de structură pe care îl parcurg cititoarele de ecran este acoperit în articolul nostru despre PDF marcat și structura de accesibilitate. Când formularul completat trebuie blocat și semnat, fluxul de lucru este descris în prezentarea detaliată a bancului de lucru pentru conformitate și semnare. Toate trei se bazează pe același motor, care este livrat ca PDF library for Delphi împreună cu API-urile de creare, formulare și semnături acoperite în alte părți ale acestui blog.