Articol tehnic

Calcularea automată a formularelor PDF în Delphi cu HotPDF

O factură sau un PDF de cheltuieli în care elementele de rând se însumează singure și data se formatează automat are nevoie de trei elemente atașate la AcroForm-ul său, iar HotPDF le adăugă pe toate trei la un fișier încărcat în Delphi: ApplyLoadedFieldCalculation pentru totaluri AFSimple_Calculate, ApplyLoadedFieldValidation pentru asistenții de formatare și validare Acrobat AF, și EnsureLoadedFieldAppearanceStream pentru a genera valoarea pe care un vizualizator o randează efectiv

HotPDF este o componentă nativă VCL PDF pentru Delphi și C++Builder, iar aceste trei metode operează pe un document pe care l-ați deschis cu LoadFromFile mai degrabă decât pe unul pe care îl construiți de la zero. Fiecare primește un indice de câmp bazat pe 0 în tabloul AcroForm /Fields — aceeași ordine pe care o returnează GetLoadedFormFieldNames — și scrie direct în dicționarul câmpului, astfel încât întreaga sarcină constă în încărcare, indexare, atașare și SaveLoadedDocument. Construirea setului de câmpuri în sine este o sarcină separată, acoperită în adăugarea câmpurilor AcroForm la un PDF încărcat în Delphi, în timp ce această pagină se referă la oferirea de funcții aritmetice și formatare unui set de câmpuri existent

De ce are nevoie un formular PDF de scripturi pentru a-și însumă propriile câmpuri?

Deoarece PDF-ul nu are un strat de tip foaie de calcul (spreadsheet). Un câmp de text conține un șir de caractere, nu o formulă, și nimic din fișier nu reevaluează un total atunci când un element se modifică, cu excepția cazului în care este atașat un script în acest scop. Mecanismul este dicționarul de acțiuni suplimentare al câmpului, /AA (ISO 32000-1 §12.7.5), care asociază evenimentele declanșatoare cu acțiuni: un declanșator de tastare (K) pe măsură ce caracterele sunt introduse, un declanșator de formatare pentru afișare, un declanșator de validare (V) la salvare (commit) și un declanșator de calcul (C) care se activează de fiecare dată când se modifică orice câmp din formular. Fiecare acțiune este o acțiune JavaScript, /S /JavaScript (ISO 32000-1 §12.6.4.14), iar scriptul pe care îl rulează este cel care efectuează suma sau reformatarea. Acrobat include o bibliotecă de funcții ajutătoare gata de utilizare — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx și celelalte —, astfel încât scriptul atașat este de obicei un simplu apel pe o singură linie către acea bibliotecă în loc de un cod personalizat

Un singur detaliu stabilește dacă un formular cu mai mulți pași oferă răspunsul corect: ordinea de calcul. Acrobat rulează fiecare câmp care conține o acțiune de calcul și le rulează în secvența listată de tabloul AcroForm /CO, nu în ordinea paginilor. ApplyLoadedFieldCalculation atașează acțiunea de calcul la câmpul specificat; atunci când un câmp calculat trimite date către altul — un subtotal citit de o linie de taxe, apoi un total general care le citește pe amândouă —, trebuie să vă asigurați că ordinea din /CO listează fiecare intrare înaintea câmpului care o consumă, altfel totalul general va citi un subtotal învechit și va rămâne în urmă cu o modificare

Cum se determină un formular PDF să calculeze totalurile în mod automat?

ApplyLoadedFieldCalculation primește indicele câmpului țintă, o operațiune și numele câmpurilor sursă pe care ar trebui să le agregeze. HotPDF scrie AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) în acțiunea de calcul a câmpului țintă, iar enumerarea THPDFFieldCalcOp selectează funcția: fccSum, fccAverage, fccProduct, fccMinimum și fccMaximum se mapează pe funcțiile Acrobat SUM, AVG, PRD, MIN și MAX. Numele sursă sunt numele câmpurilor AcroForm, aceleași șiruri pe care le returnează GetLoadedFormFieldNames, astfel încât obținerea unui total pentru trei elemente și a unei taxe de transport reprezintă un singur apel

var
  Pdf: THotPDF;
  Names: THPDFAnsiStringArray;
  i, TotalIdx: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('invoice.pdf');
    Names := Pdf.GetLoadedFormFieldNames;
    TotalIdx := -1;
    for i := 0 to High(Names) do
      if Names[i] = 'grandTotal' then TotalIdx := i;
    if TotalIdx >= 0 then
    begin
      Pdf.ApplyLoadedFieldCalculation(TotalIdx, fccSum,
        ['lineTotal.1', 'lineTotal.2', 'lineTotal.3', 'shipping']);
      Pdf.EnsureLoadedFieldAppearanceStream(TotalIdx);
    end;
    Pdf.SaveLoadedDocument('invoice-live.pdf');
  finally
    Pdf.Free;
  end;
end;

Formatarea și validarea câmpurilor cu setul de asistenți AF

ApplyLoadedFieldValidation atașează scripturile de formatare, tastare și validare într-un singur apel, iar valoarea THPDFFieldValidationKind pe care o transmiteți stabilește ce asistent Acrobat va fi conectat. Dacă transmiteți fvkCurrency, HotPDF scrie AFNumber_Format(2,0,0,0,"$ ",true) ca script de formatare cu funcția corespunzătoare AFNumber_Keystroke; fvkNumber folosește "." pentru argumentul separator, fvkPercentage emite AFPercent_Format, iar fvkDate emite AFDate_FormatEx("dd/mm/yyyy"), cu excepția cazului în care transmiteți propria mască în argumentul DateFormat. Formatele speciale — fvkZipCode, fvkSSN și fvkPhone — se mapează pe AFSpecial_Format cu codul pe care Acrobat îl rezervă pentru fiecare, iar fvkArbitraryMask primește un șir de mască prin argumentul Mask pentru orice element neacoperit de aceste presetări. Pentru tipurile numerice, HotPDF atașează de asemenea un script AFRange_Validate, astfel încât intrările din afara intervalului sunt refuzate la confirmare (commit)

// Format the invoice date as ISO, the amount as currency, and mask a tax id
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

De ce trebuie să regenerați fluxul de aspect (appearance stream)?

Deoarece șirul de valori și pixelii pe care îi desenează un vizualizator sunt două lucruri diferite în PDF, iar modificarea unuia nu o modifică pe cealaltă. Un câmp își stochează valoarea în /V, dar ceea ce randează un cititor provine din fluxul de aspect al câmpului, /AP /N. Editați /V prin graful de obiecte și un vizualizator strict va afișa în continuare aspectul vechi până când ceva îl regenerează. Dicționarul formularului conține un flag /NeedAppearances care solicită vizualizatorului să reconstruiască fiecare aspect la deschidere, iar Acrobat respectă această solicitare — dar motoarele de randare integrate în browsere, cititoarele mobile, serverele de imprimare și generatoarele de miniaturi ignoră frecvent acest flag și desenează orice flux este deja prezent sau chiar nimic. EnsureLoadedFieldAppearanceStream elimină acest decalaj prin construirea Form XObject-ului /AP /N în sine, din câmpurile /DA, /V și /Rect, astfel încât valoarea randată călătorește în interiorul fișierului în loc să depindă de vizualizator pentru a o redesena

// Bake an appearance for every field before saving, so values render everywhere
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Unde nu mai funcționează scripturile AF

Fiecare script AF* depinde de prezența unui motor JavaScript în vizualizator, iar majoritatea vizualizatoarelor nu au unul. Acrobat și câteva produse desktop rulează aceste funcții ajutătoare; vizualizatoarele PDF integrate în Chrome, Edge, Firefox și pe marile platforme mobile, alături de rasterizatoarele de pe server și fluxurile de imprimare, nu execută deloc JavaScript pentru formulare. În aceste cititoare, calculul nu se activează niciodată, iar formatul nu se aplică — câmpul afișează pur și simplu fluxul de aspect cu care a fost salvat ultima dată, acesta fiind motivul exact pentru care crearea pre-randată a /AP /N este esențială. Tratați stratul AF* ca pe o facilitate pentru utilizatorii cu cititoare compatibile cu scripturi, niciodată ca pe o garanție. Orice total care trebuie să fie corect pe server — o valoare a facturii pentru facturare, o cifră de taxe pentru depunere — trebuie reevaluată de partea dvs. după ce formularul este trimis înapoi, deoarece nu puteți presupune că cititorul care l-a completat a rulat vreo linie din scriptul dvs

Încă un caz merită menționat: formularele primite sub formă de XFA. Un formular XFA dinamic conține propriul său motor de calcul într-un payload XML despre care asistenții AF* nu știu nimic, astfel încât atașarea AFSimple_Calculate la un câmp XFA nu are niciun efect. Aplatizați mai întâi formularul XFA într-un AcroForm simplu, așa cum este descris în aplatizarea XFA în AcroForm în Delphi, iar cele trei metode de aici se vor aplica apoi câmpurilor AcroForm rezultate exact la fel ca în cazul oricărui alt formular încărcat

AcroForm-urile cu calcul automat și auto-formatare se rezumă la trei apeluri pe un document încărcat — atașarea calculului, atașarea formatării și validării, și scrierea aspectului (appearance) —, urmate de SaveLoadedDocument. API-ul de formular încărcat prezentat aici face parte din HotPDF Component pentru Delphi și C++Builder, care oferă documentația completă pentru enumerările THPDFFieldValidationKind și THPDFFieldCalcOp alături de restul elementelor AcroForm