Technický článek

Automaticky vypočítávané PDF formuláře v Delphi s HotPDF

Faktura nebo PDF s výdaji, kde se řádkové položky samy sčítají a datum se samo formátuje, vyžaduje tři věci připojené ke svému AcroForm, a HotPDF přidává všechny tři do načteného souboru v Delphi: `ApplyLoadedFieldCalculation` pro součty `AFSimple_Calculate`, `ApplyLoadedFieldValidation` pro pomocníky formátu a validace Acrobat AF, a `EnsureLoadedFieldAppearanceStream` pro vytvoření (upečení) vzhledového toku, který prohlížeč skutečně vykreslí

HotPDF je nativní komponenta VCL PDF pro Delphi a C++Builder a tyto tři metody pracují s dokumentem, který jste otevřeli pomocí `LoadFromFile`, nikoli s dokumentem, který vytváříte od nuly. Každá z nich přijímá index pole od nuly do pole `/Fields` v AcroForm — ve stejném pořadí, v jakém jej vrací `GetLoadedFormFieldNames` — a zapisuje přímo do slovníku pole, takže celá práce spočívá v načtení, indexování, připojení a volání `SaveLoadedDocument`. Sestavení samotné sady polí je samostatný úkol, kterým se zabývá poznámka o přidávání polí AcroForm do načteného PDF v Delphi, zatímco tato stránka pojednává o tom, jak stávající sadě polí dodat aritmetiku a formátování

Proč PDF formulář potřebuje skripty ke sčítání vlastních polí?

Protože PDF nemá žádnou tabulkovou vrstvu. Textové pole uchovává řetězec, nikoli vzorec, a nic v souboru nepřepočítá celkovou částku, když se řádková položka změní, pokud k tomu není připojen skript. Mechanismem je slovník doplňkových akcí pole, `/AA` (ISO 32000-1 §12.7.5), který mapuje spouštěcí události na akce: spouštěč stisku klávesy (`K`) při příchodu znaků, spouštěč formátu pro zobrazení, spouštěč validace (`V`) při potvrzení a spouštěč výpočtu (`C`), který se spustí pokaždé, když se změní jakékoli pole ve formuláři. Každá akce je akcí JavaScriptu, `/S /JavaScript` (ISO 32000-1 §12.6.4.14), a skript, který spouští, je to, co skutečně provádí součet nebo přeformátování. Acrobat se dodává s knihovnou hotových pomocníků — `AFSimple_Calculate`, `AFNumber_Format`, `AFDate_FormatEx` a dalšími —, takže připojený skript je obvykle jednořádkovým voláním do této knihovny namísto vlastního kódu

Jeden detail rozhoduje o tom, zda víceúrovňový formulář dává správný výsledek: pořadí výpočtů. Acrobat spouští každé pole, které nese akci výpočtu, a spouští je v pořadí, v jakém je uvádí pole `/CO` v AcroForm, nikoli v pořadí stránek. `ApplyLoadedFieldCalculation` připojí akci výpočtu k poli, které pojmenujete; když jedno vypočítávané pole napájí jiné — například mezisoučet, který čte řádek daně, a poté celkový součet, který čte oba —, musíte potvrdit, že pořadí `/CO` uvádí každý vstup před polem, které jej spotřebovává, jinak bude celkový součet číst zastaralý mezisoučet a bude zaostávat o jednu editaci pozadu

Jak zařídit, aby PDF formulář automaticky počítal součty?

`ApplyLoadedFieldCalculation` přijímá index cílového pole, operaci a názvy zdrojových polí, která má agregovat. HotPDF zapíše `AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...])` do akce výpočtu cílového pole a výčet `THPDFFieldCalcOp` vybere funkci: `fccSum`, `fccAverage`, `fccProduct`, `fccMinimum` a `fccMaximum` se mapují na Acrobat SUM, AVG, PRD, MIN a MAX. Zdrojové názvy jsou názvy polí AcroForm, stejné řetězce, jaké vrací `GetLoadedFormFieldNames`, takže celkový součet nad třemi řádkovými položkami a poplatkem za dopravu představuje jediné volání

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;

Volání `EnsureLoadedFieldAppearanceStream` u celkového součtu není kosmetické. Výpočet probíhá uvnitř prohlížeče, takže dokud soubor neotevře čtečka podporující skripty, celkové pole nemá žádný vlastní vykreslený obsah. Upečení vzhledového toku (appearance stream) mu dává hodnotu, kterou zobrazí mezitím, což je to, co převezme tisková úloha nebo generátor náhledů, když skript nikdy nespustí

Formátování a validace polí pomocí sady pomocníků AF

`ApplyLoadedFieldValidation` připojí skripty formátu, stisku klávesy a validace v jediném volání a předaný typ `THPDFFieldValidationKind` rozhodne, který pomocník Acrobat se připojí. Pokud předáte `fvkCurrency`, HotPDF zapíše `AFNumber_Format(2,0,0,0,"$ ",true)` jako formátovací skript s odpovídajícím `AFNumber_Keystroke`; `fvkNumber` používá `"."` pro argument oddělovače, `fvkPercentage` emituje `AFPercent_Format` a `fvkDate` emituje `AFDate_FormatEx("dd/mm/yyyy")`, pokud nepředáte vlastní masku v argumentu `DateFormat`. Speciální formáty — `fvkZipCode`, `fvkSSN` a `fvkPhone` — se mapují na `AFSpecial_Format` s kódem, který si Acrobat pro každý z nich vyhrazuje, a `fvkArbitraryMask` přijímá maskovací řetězec prostřednictvím argumentu `Mask` pro cokoli, co tyto předvolby nepokrývají. Pro číselné typy HotPDF také připojuje skript `AFRange_Validate`, takže vstup mimo rozsah je při odeslání odmítnut

// Formátovat datum faktury jako ISO, částku jako měnu a maskovat DIČ
Pdf.ApplyLoadedFieldValidation(DateIdx,   fvkDate,          'yyyy-mm-dd', '');
Pdf.ApplyLoadedFieldValidation(AmountIdx, fvkCurrency,      '', '');
Pdf.ApplyLoadedFieldValidation(TaxIdIdx,  fvkArbitraryMask, '', '999-99-9999');
Pdf.EnsureLoadedFieldAppearanceStream(AmountIdx);

Pomocníci AF pokrývají běžné tvary, takže pro ně nemusíte JavaScript psát ručně. Když pole potřebuje logiku, kterou žádná předvolba nevyjadřuje — kontrolní součet, pravidlo pro více polí, vlastní regulární výraz —, připojte surový skript sami pomocí metod akcí na úrovni pole popsaných v sestavování polí AcroForm a akcí s HotPDF, které přijímají přímo řetězec JavaScriptu. Obě cesty zapisují do stejného slovníku `/AA`, takže přednastavený formát měny na jednom poli a ručně psaná validace na jiném v jednom dokumentu bez problémů koexistují

Proč musíte regenerovat vzhledový tok (appearance stream)?

Protože řetězec hodnoty a pixely, které prohlížeč vykresluje, jsou v PDF dvě různé věci a změna jedné nezmění druhou. Pole ukládá svou hodnotu v `/V`, ale to, co čtečka vykresluje, pochází ze vzhledového toku pole, `/AP /N`. Upravte `/V` prostřednictvím objektového grafu a přísný prohlížeč bude stále zobrazovat starý vzhled, dokud jej něco nezregeneruje. Slovník formuláře nese příznak `/NeedAppearances`, který žádá prohlížeč o obnovení všech vzhledů při otevření, a Acrobat jej respektuje — avšak vykreslovače integrované v prohlížečích, mobilní čtečky, tiskové servery a generátory náhledů jej často ignorují a vykreslují jakýkoli již přítomný tok, případně vůbec nic. `EnsureLoadedFieldAppearanceStream` tuto mezeru uzavírá tím, že samo sestaví objekt Form XObject `/AP /N` z hodnot pole `/DA`, `/V` a `/Rect`, takže vykreslená hodnota putuje uvnitř souboru, místo aby závisela na překreslení prohlížečem

// Upéct vzhled pro každé pole před uložením, aby se hodnoty vykreslovaly všude
Names := Pdf.GetLoadedFormFieldNames;
for i := 0 to High(Names) do
  Pdf.EnsureLoadedFieldAppearanceStream(i);
Pdf.SaveLoadedDocument('invoice-flat-appearance.pdf');

Kde skripty AF přestávají fungovat

Každý skript AF* závisí na tom, zda má prohlížeč JavaScript engine, a většina prohlížečů jej nemá. Acrobat a hrstka desktopových produktů tyto pomocníky spouští; PDF prohlížeče vestavěné v Chrome, Edge, Firefoxu a hlavních mobilních platformách, spolu s rastrovacími moduly na straně serveru a tiskovými frontami, nespouštějí žádný JavaScript formulářů. V těchto čtečkách se výpočet nikdy nespustí a formát se nikdy neaplikuje — pole jednoduše zobrazuje jakýkoli vzhledový tok, se kterým bylo naposledy uloženo, což je přesně důvod, proč na upečení `/AP /N` záleží. Považujte vrstvu AF* za usnadnění pro podmnožinu uživatelů ve čtečce podporující skripty, nikdy ne za záruku. Jakýkoli celkový součet, který musí být na serveru správně — částka faktury, kterou budete účtovat, údaj o dani, který budete podávat —, musí být přepočítán na vaší straně poté, co se formulář vrátí, protože nemůžete předpokládat, že čtečka, která jej vyplnila, spustila jediný řádek vašeho skriptu

Stojí za to jmenovat ještě jeden případ: formuláře, které dorazily jako XFA. Dynamický formulář XFA nese svůj vlastní výpočetní engine v XML těle, o kterém pomocníci AF* nic nevědí, takže připojení `AFSimple_Calculate` k poli XFA ničeho nedosáhne. Nejprve formulář XFA zploštěte na běžný AcroForm, jak je popsáno v zploštění XFA na AcroForm v Delphi, a zde uvedené tři metody pak budou platit pro výsledná pole AcroForm přesně stejně jako pro jakýkoli jiný načtený formulář

Automaticky vypočítávané a samoformátující se AcroFormy se omezují na tři volání nad načteným dokumentem — připojení výpočtu, připojení formátu a validace, upečení vzhledu — následované `SaveLoadedDocument`. Zde popsané API pro načtené formuláře je součástí komponenty HotPDF Component pro Delphi a C++Builder, která dokumentuje kompletní výčty `THPDFFieldValidationKind` a `THPDFFieldCalcOp` spolu se zbytkem rozhraní AcroForm