Artykuł techniczny

Automatycznie obliczane formularze PDF w Delphi za pomocą HotPDF

Faktura lub rozliczenie wydatków w formacie PDF, w których pozycje same się sumują, a data automatycznie formatuje, wymaga dodania trzech rzeczy do formularza AcroForm. Komponent HotPDF pozwala na wdrożenie wszystkich trzech elementów w załadowanym pliku w środowisku Delphi: metoda ApplyLoadedFieldCalculation służy do obliczeń sum za pomocą AFSimple_Calculate, ApplyLoadedFieldValidation odpowiada za formatowanie i walidację przy użyciu pomocniczych funkcji Acrobat AF, a EnsureLoadedFieldAppearanceStream służy do generowania strumienia wyglądu wartości (appearance stream), którą przeglądarka faktycznie rysuje na ekranie

HotPDF to natywny komponent VCL PDF dla środowisk Delphi i C++Builder. Te trzy metody działają na dokumencie otwartym za pomocą LoadFromFile, a nie tworzonym od zera. Każda z nich przyjmuje indeks pola (liczony od zera) w tablicy AcroForm /Fields — w takiej samej kolejności, w jakiej zwraca je metoda GetLoadedFormFieldNames — i zapisuje dane bezpośrednio do słownika pola. Całe zadanie sprowadza się więc do wczytania, wskazania indeksu, dołączenia skryptu i wywołania SaveLoadedDocument. Samo budowanie zestawu pól to osobne zadanie opisane w artykule o dodawaniu pól AcroForm do załadowanego pliku PDF w Delphi, natomiast ten artykuł poświęcony jest nadawaniu istniejącym polom właściwości obliczeniowych i formatowania

Dlaczego formularz PDF potrzebuje skryptów do sumowania własnych pól?

Ponieważ format PDF nie posiada warstwy arkusza kalkulacyjnego. Pole tekstowe przechowuje ciąg znaków, a nie formułę, i nic w pliku nie przeliczy sumy po zmianie pozycji, chyba że dołączony zostanie skrypt realizujący to zadanie. Wykorzystywanym mechanizmem jest słownik dodatkowych akcji pola, /AA (ISO 32000-1 §12.7.5), który mapuje zdarzenia wyzwalające na akcje: zdarzenie naciśnięcia klawisza (K) przy wpisywaniu znaków, zdarzenie formatowania do wyświetlania, zdarzenie walidacji (V) przy zatwierdzaniu oraz zdarzenie obliczania (C) uruchamiane przy każdej zmianie dowolnego pola w formularzu. Każda akcja jest akcją JavaScript, /S /JavaScript (ISO 32000-1 §12.6.4.14), a uruchamiany przez nią skrypt wykonuje sumowanie lub zmianę formatu. Program Acrobat dostarcza bibliotekę gotowych funkcji pomocniczych — AFSimple_Calculate, AFNumber_Format, AFDate_FormatEx i inne — więc dołączany skrypt to zazwyczaj jednolinijkowe wywołanie z tej biblioteki, a nie dedykowany kod

O poprawności wyniku w wieloetapowych formularzach decyduje jeden szczegół: kolejność obliczeń. Program Acrobat uruchamia każde pole posiadające akcję obliczeniową i wykonuje je w kolejności określonej w tablicy AcroForm /CO, a nie w kolejności stron. Metoda ApplyLoadedFieldCalculation dołącza akcję obliczeniową do wskazanego pola. Gdy jedno pole obliczane zasila drugie — np. wartość pośrednia (subtotal) jest czytana przez pole podatku, a następnie pole sumy całkowitej (grand total) czyta obie wartości — musisz upewnić się, że kolejność w tablicy /CO wymienia każde pole wejściowe przed polem, które z niego korzysta. W przeciwnym razie suma całkowita pobierze nieaktualną wartość pośrednią i będzie spóźniona o jedną edycję

Jak sprawić, aby formularz PDF automatycznie obliczał sumy?

Metoda ApplyLoadedFieldCalculation przyjmuje indeks pola docelowego, operację oraz nazwy pól źródłowych, które mają zostać zsumowane. Komponent HotPDF zapisuje wyrażenie AFSimple_Calculate("SUM", ["lineTotal.1", "lineTotal.2", ...]) w akcji obliczeniowej pola docelowego, a typ wyliczeniowy THPDFFieldCalcOp wskazuje funkcję: wartości fccSum, fccAverage, fccProduct, fccMinimum oraz fccMaximum odpowiadają funkcjom SUM, AVG, PRD, MIN i MAX programu Acrobat. Nazwy źródłowe to nazwy pól AcroForm (te same ciągi znaków, które zwraca GetLoadedFormFieldNames), więc zdefiniowanie sumy dla trzech pozycji i kosztu wysyłki sprowadza się do jednego wywołania

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;

Wywołanie EnsureLoadedFieldAppearanceStream dla pola sumy nie ma charakteru wyłącznie kosmetycznego. Obliczenia są wykonywane wewnątrz przeglądarki, więc dopóki dokument nie zostanie otwarty w programie obsługującym skrypty, pole sumy nie posiada wyrenderowanej zawartości. Zapisanie strumienia wyglądu (appearance stream) dostarcza wartość liczbową do wyświetlenia na ten czas, co jest kluczowe np. przy drukowaniu lub generowaniu miniatur, gdy skrypty nie są wykonywane

Formatowanie i walidacja pól za pomocą pakietu funkcji pomocniczych AF

Metoda ApplyLoadedFieldValidation dołącza skrypty formatowania, naciśnięcia klawisza i walidacji w jednym wywołaniu, a przekazana wartość THPDFFieldValidationKind decyduje, która funkcja pomocnicza programu Acrobat zostanie powiązana. Przekazanie fvkCurrency sprawia, że HotPDF zapisuje AFNumber_Format(2,0,0,0,"$ ",true) jako skrypt formatowania wraz z pasującym AFNumber_Keystroke. fvkNumber używa znaku "." jako separatora, fvkPercentage generuje AFPercent_Format, a fvkDate generuje AFDate_FormatEx("dd/mm/yyyy"), chyba że przekażesz własną maskę w argumencie DateFormat. Formaty specjalne — fvkZipCode, fvkSSN oraz fvkPhone — mapują się na AFSpecial_Format z kodem rezerwowanym przez program Acrobat dla każdego z nich, natomiast fvkArbitraryMask przyjmuje niestandardową maskę w argumencie Mask dla wszystkiego, czego nie obejmują szablony domyślne. W przypadku typów numerycznych HotPDF dołącza również skrypt AFRange_Validate, dzięki czemu wartości spoza zakresu są odrzucane przy próbie ich zatwierdzenia

// 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);

Funkcje pomocnicze AF obejmują typowe formaty, więc nie musisz pisać dla nich kodu JavaScript ręcznie. Gdy pole wymaga logiki, której nie opisuje żaden szablon domyślny — np. sumy kontrolnej, reguły między kilkoma polami czy niestandardowego wyrażenia regularnego — dołącz surowy skrypt samodzielnie, korzystając z metod akcji na poziomie pola opisanych w artykule o budowaniu pól i akcji AcroForm za pomocą HotPDF, które przyjmują bezpośrednio ciąg znaków JavaScript. Obie te drogi zapisują dane do tego samego słownika /AA, więc gotowy format waluty na jednym polu i ręcznie napisana walidacja na innym mogą współistnieć w tym samym dokumencie

Dlaczego musisz wygenerować strumień wyglądu na nowo?

Ponieważ w formacie PDF wartość tekstowa i piksele rysowane przez przeglądarkę to dwie różne rzeczy, a zmiana jednej z nich nie wpływa automatycznie na drugą. Pole przechowuje swoją wartość w /V, ale przeglądarka renderuje zawartość na podstawie strumienia wyglądu pola, /AP /N. Jeśli zmodyfikujesz /V poprzez graf obiektów, restrykcyjna przeglądarka nadal będzie wyświetlać stary wygląd, dopóki nie zostanie on wygenerowany na nowo. Słownik formularza zawiera flagę /NeedAppearances, która żąda od przeglądarki odbudowania każdego wyglądu przy otwarciu pliku i Acrobat spełnia to żądanie — jednak silniki wbudowane w przeglądarki internetowe, czytniki mobilne, serwery druku i generatory miniatur często ignorują tę flagę i rysują strumień, który jest już obecny w pliku, lub nie rysują niczego. Metoda EnsureLoadedFieldAppearanceStream eliminuje tę lukę poprzez samodzielne zbudowanie obiektu Form XObject /AP /N na podstawie parametrów pola /DA, /V oraz /Rect, dzięki czemu wyrenderowana wartość jest zapisana bezpośrednio w pliku, a nie zależna od przeglądarki

// 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');

Gdzie skrypty AF przestają działać

Każdy skrypt z serii AF* wymaga obecności silnika JavaScript w przeglądarce, a większość czytników go nie posiada. Program Acrobat oraz kilka produktów desktopowych uruchamia te funkcje; jednak przeglądarki PDF wbudowane w Chrome, Edge, Firefox oraz główne platformy mobilne, a także serwerowe rasteryzatory i potoki drukowania nie uruchamiają żadnego kodu JavaScript w formularzach. W tych czytnikach obliczenia nigdy się nie wywołają, a formatowanie nie zostanie zastosowane — pole po prostu wyświetli strumień wyglądu, z jakim plik został ostatnio zapisany, co wyjaśnia, dlaczego generowanie /AP /N jest tak ważne. Traktuj warstwę AF* jako udogodnienie dla podzbioru użytkowników korzystających z czytników obsługujących skrypty, a no-go dla gwarancji działania. Każda suma, która musi być poprawna po stronie serwera — np. kwota faktury do rozliczenia czy wartość podatku do zadeklarowania — musi zostać ponownie obliczona po Twojej stronie po otrzymaniu formularza, ponieważ nie możesz zakładać, że czytnik, który go uzupełnił, wykonał chociażby jedną linię Twojego skryptu

Warto wspomnieć o jeszcze jednym przypadku: formularzach dostarczanych jako XFA. Dynamiczny formularz XFA zawiera własny silnik obliczeniowy wewnątrz struktury XML, o którym funkcje pomocnicze AF* nic nie wiedzą, więc przypisanie AFSimple_Calculate do pola XFA nic nie daje. Należy najpierw spłaszczyć formularz XFA do postaci zwykłego AcroForm (zgodnie z instrukcją w artykule o spłaszczaniu XFA do AcroForm w Delphi), a wtedy opisane tutaj trzy metody będą miały zastosowanie do wynikowych pól AcroForm dokładnie tak samo, jak w przypadku każdego innego załadowanego formularza

Automatycznie obliczane i formatujące się formularze AcroForm sprowadzają się do trzech wywołań na załadowanym dokumencie — dołączenia obliczeń, dołączenia formatu i walidacji oraz wygenerowania wyglądu — po których następuje wywołanie SaveLoadedDocument. Przedstawione tutaj interfejsy API formularza są częścią komponentu HotPDF Component dla Delphi i C++Builder, którego dokumentacja opisuje pełne typy wyliczeniowe THPDFFieldValidationKind oraz THPDFFieldCalcOp wraz z pozostałą częścią API formularzy AcroForm