Artykuł techniczny

Interaktywne formularze PDF w Delphi: Akcje i JavaScript

Samo pole formularza PDF to tylko pole przechowujące wartość. To, co sprawia, że formularz zachowuje się jak mała aplikacja, to dołączona do niego akcja: kliknięcie ukrywające sekcję, pobranie zapisanych wartości z pliku, przejście do ostatniej strony lub uruchomienie skryptu sumującego kolumnę. Nic z tego nie znajduje się w samym polu. Żyje to w słowniku akcji, a standard ISO 32000-1 organizuje całą tę rodzinę w §12.6. W tym artykule omówiono akcje, po które program w Delphi sięga najczęściej, oraz pokazano, jak PDF Library for Delphi łączy każdą z nich z polem lub linkiem

Model pojęciowy, o którym warto pamiętać, zakłada, że pole i akcja to osobne obiekty połączone referencją. Adnotacja widżetu lub adnotacja linku przenosi akcję w swoim wpisie /A. Akcja identyfikuje pole, na którym operuje, za pomocą nazwy (tytułu), a nie indeksu, więc tytuł nadany polu jest uchwytem, którego każda kolejna akcja używa do jego odnalezienia. Gdy ten podział stanie się jasny, API przestaje wyglądać jak losowy zestaw wywołań, a zaczyna jawić się jako jeden wzorzec zastosowany do czterech rodzajów operacji

Diagram PDF Library for Delphi adnotacji łącza lub widżetu PDF odnoszącej się do jednego słownika akcji rozchodzącego się na akcje nazwaną, hide, import-data i JavaScript, przy czym karta import-data jest oflagowana jako wykluczona z PDF/A
Jeden słownik akcji za łączem lub widżetem zasila cztery różne czasowniki — ruch widocznego obszaru, widoczność, import z dysku i skrypty

Nazwane akcje: nawigacja bez numeru strony

Najprostsze akcje nie przenoszą żadnych parametrów. Standard ISO 32000-1 §12.6.4.11, tabela 194, definiuje nazwane akcje (named actions): przeglądarka interpretuje symboliczną nazwę w czasie wykonywania, zamiast podążać za zapisanym celem. Powszechnie obsługiwane są cztery nazwy i są to dokładnie te, których czytelnik oczekuje od paska narzędzi: NextPage, PrevPage, FirstPage i LastPage. Ponieważ cel jest względny w stosunku do strony, którą przeglądarka aktualnie wyświetla, przycisk Dalej zbudowany w ten sposób działa na każdej stronie bez konieczności obliczania celu przez programistę

W PDF Library for Delphi nazwana akcja jest dołączana do prostokąta aktywny obszar (hotspot) na bieżącej stronie. Czwarty i piąty argument typu integer wybierają operację oraz wygląd

// NamedActionType: 0 = NextPage, 1 = PrevPage, 2 = FirstPage, 3 = LastPage
// Bit 0 Options (wartość 1) rysuje obramowanie wokół obszaru aktywnego
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);   // przejdź do ostatniej strony

Nie ma tu celu, który trzeba by synchronizować, i o to właśnie chodzi. Nazwana akcja przetrwa wstawianie i usuwanie stron, ponieważ w ogóle nie wskazuje konkretnej strony. Kontrastuje to z jawnym linkiem typu go-to, który przechowuje indeks strony docelowej, wymagający ponownej numeracji w momencie, gdy dokument się rozrasta

Akcja Hide i pułapka związana z tablicą

Akcja Hide, opisana w ISO 32000-1 §12.6.4.10, tabela 196, przełącza widoczność jednego lub więcej pól. Jest to najczystszy sposób na zbudowanie zachowania pokazywania i ukrywania bez użycia skryptów i doskonale nadaje się dla linków typu Pokaż szczegóły lub dwóch wykluczających się wzajemnie paneli, gdzie ujawnienie jednego ukrywa drugi. Akcja ta niesie cel w swoim wpisie /T oraz wartość logiczną /H, która decyduje o kierunku: ukryj, gdy ma wartość true, pokaż, gdy false

Subtelność tkwi w sposobie kodowania tego celu i jest to ten rodzaj szczegółu, który sprawia, że formularz działa na komputerze dewelopera, a u klienta już nie. Gdy akcja wskazuje pojedyncze pole, wpis /T jest zapisywany jako pojedynczy ciąg tekstowy. Gdy wskazuje kilka pól, /T jest zapisywany jako tablica ciągów tekstowych. Starsze przeglądarki nie traktują tablicy jednoelementowej tak samo jak zwykłego ciągu znaków, więc kodowanie musi zależeć od liczby pól: pojedyncza nazwa musi być wygenerowana jako ciąg znaków, a nie tablica o długości jeden, aby najszersza gama czytników mogła ją obsłużyć. PDF Library for Delphi podejmuje tę decyzję za Ciebie. Przekazujesz nazwy pól rozdzielone przecinkami, średnikami lub znakami nowej linii, a program zapisujący generuje pojedynczy ciąg znaków dla jednej nazwy oraz tablicę dla dwóch lub więcej

Diagram PDF Library for Delphi reguł kodowania akcji Hide PDF, gdzie dokładnie jedna nazwa pola staje się ciągiem tekstowym /T, a dwie lub więcej nazw staje się tablicą ciągów, plus notki o kierunku flagi ukrycia, bezpieczeństwie PDF/A i w pełni kwalifikowanych tytułach pól
Zapisujący rozgałęzia się według liczby pól, więc pojedyncza nazwa wychodzi jako goły ciąg, który honorują starsze czytniki, a dwie lub więcej staje się tablicą przełączaną razem
// Niezerowy HideFlag ukrywa wymienione pola (/H true); zero je pokazuje
// Jedna nazwa -> /T jest łańcuchem tekstowym. Dwie lub więcej -> /T jest tablicą łańcuchów
Pdf.AddLinkToHideField(40, 700, 90, 18, 'ShippingAddress', 1, 1);
Pdf.AddLinkToHideField(140, 700, 90, 18,
  'ShippingName,ShippingAddress,ShippingZip', 1, 1);

Ponieważ akcja nie odwołuje się do żadnego zewnętrznego zasobu, pozostaje zgodna z PDF/A. Przekazywane nazwy to w pełni kwalifikowane nazwy pól, dlatego pole podrzędne w grupie musi być adresowane za pomocą pełnej ścieżki z kropkami, a nie tylko samej nazwy liścia

ImportData: wstępne wypełnianie z FDF

Tam, gdzie akcja Hide reorganizuje to, co już znajduje się na stronie, akcja importowania danych pobiera wartości z zewnątrz. Standard ISO 32000-1 §12.6.4.8, tabela 198, definiuje ją jako akcję, która wypełnia formularz AcroForm z pliku Forms Data Format (FDF) na dysku. Jest to akcja stojąca za kontrolkami typu Przeładuj przykładowe dane lub Przywróć domyślne, gdzie plik FDF jest dostarczany obok dokumentu PDF i zawiera kanoniczne wartości pól. Wywołanie to odzwierciedla inne akcje, przyjmując prostokąt aktywnego obszaru, ścieżkę do pliku FDF i maskę bitową wyglądu: Pdf.AddLinkToImportData(40, 660, 120, 18, 'defaults.fdf', 1). Plik nie musi istnieć w momencie tworzenia pliku PDF, ale musi być obecny, gdy użytkownik kliknie link, a wszelkie ukośniki odwrotne w ścieżce są automatycznie zamieniane na ukośniki zgodne ze standardem PDF

Jedno ograniczenie warto sformułować jasno, ponieważ bywa zaskoczeniem. Akcja importu danych wskazuje na plik zewnętrzny, dlatego nie jest dozwolona w formacie PDF/A. Gdy dokument działa w trybie PDF/A, wywołanie zwraca zero i nic nie dodaje, zamiast generować plik, który nie przejdzie walidacji. Jeśli Twój proces przetwarzania ma na celu wygenerowanie wyjściowego archiwum, wstępne wypełnianie must nastąpić w momencie generowania dokumentu poprzez bezpośrednie zapisanie wartości pól, a nie odłożenie tego do momentu kliknięcia

JavaScript: pakiety globalne i skrypty powiązane z akcjami

W przypadku logiki wykraczającej poza pokazywanie, ukrywanie i importowanie, rodzina akcji sięga po JavaScript na poziomie dokumentu. Skrypt może znajdować się w dwóch różnych miejscach, a różnica między nimi ma kluczowe znaczenie. Pakiet JavaScript na poziomie dokumentu jest zapisywany raz dla całego pliku i uruchamia się przy otwarciu dokumentu, co czyni go odpowiednim miejscem dla definicji funkcji i współdzielonego stanu. Skrypt powiązany z akcją (per-action) jest dołączony do jednego linku lub pola i uruchamia się tylko wtedy, gdy ten obiekt zostanie aktywowany, co czyni go odpowiednim miejscem na tę jedną linijkę kodu, która wywołuje funkcję zdefiniowaną już w pakiecie globalnym

PDF Library for Delphi udostępnia obie te opcje. AddGlobalJavaScript zapisuje nazwany pakiet na poziomie dokumentu; ponowne użycie nazwy zastępuje to, co było pod nią zapisane. Z kolei AddLinkToJavaScript dołącza skrypt do aktywnego obszaru, dzięki czemu kliknięcie go powoduje uruchomienie skryptu

Diagram PDF Library for Delphi dwupoziomowego modelu JavaScript PDF, gdzie globalny pakiet na poziomie dokumentu definiuje recalcTotal przy otwarciu dokumentu, a każde łącze niesie jednoliniowy skrypt akcji wywołujący go po kliknięciu
Zdefiniuj recalcTotal raz w pakiecie na poziomie dokumentu i pozwól każdemu klikalnemu hotspotowi sięgać do niego jedno wierszowym wywołaniem akcji
// Pakiet poziomu dokumentu: zdefiniuj funkcję wielokrotnego użytku raz
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);' +
  '}');

// Skrypt akcji na łączu: po prostu wywołaj współdzieloną funkcję
Pdf.AddLinkToJavaScript(40, 620, 100, 18, 'recalcTotal();', 1);

Trzymanie funkcji w pakiecie globalnym, a wywołania w linku, nie wynika jedynie z preferencji stylistycznych. Pozwala to uniknąć duplikowania tej samej zawartości w każdej kontrolce, która jej potrzebuje. Oznacza to również, że przeglądarka z wyłączoną obsługą skryptów po kliknięciu po prostu nic nie zrobi, zamiast zawiesić się na błędnie sformułowanym obiekcie wierszowym. Ponadto pozwala to zachować niewielki rozmiar wpisów powiązanych z poszczególnymi akcjami, co ułatwia czytanie struktury pliku podczas jego późniejszej inspekcji

Pola, pola podrzędne i zamrażanie wyniku

Akcje potrzebują pól, na których mogą operować, więc warto zobaczyć, jak powstaje pole. Funkcja NewFormField tworzy pole na bieżącej stronie i zwraca jego indeks; typ całkowity określa rodzaj pola, gdzie 1 to Text, 2 to Pushbutton, 3 to Checkbox, 4 to Radiobutton, 5 to Choice, 6 to Signature, a 7 to Parent, które posiada dzieci, ale samo nic nie rysuje. Przekazywany tytuł nie może zawierać kropki, ponieważ kropka służy jako separator w w pełni kwalifikowanych nazwach, których akcje używają do adresowania pól podrzędnych

Grupy radiowe i formularze hierarchiczne są budowane poprzez przypisywanie pól podrzędnych do pola nadrzędnego. NewChildFormField dodaje element podrzędny pod nazwanym elementem nadrzędnym, a w przypadku pól opcji (radio) i wyboru (choice) AddFormFieldSub dodaje poszczególne opcje i zwraca tymczasowy indeks służący do pozycjonowania każdej z nich. Gdy faza interaktywna dobiegnie końca i chcesz zamrozić pole, aby jego aktualny wygląd stał się stałą zawartością strony, FlattenFormField rysuje pole na stronie i usuwa je z formularza. Po operacji spłaszczania (flatten) indeksy kolejnych pól przesuwają się w dół o jeden, o czym należy pamiętać, jeśli spłaszczasz wiele pól w pętli

var
  Pdf: TPDFlib;
  FldShip: Integer;
begin
  Pdf := TPDFlib.Create;
  try
    Pdf.SetOrigin(1);          // top-left origin
    Pdf.SetPageSize('A4');
    Pdf.NewPage;

    // Pole tekstowe, które akcja Hide wskaże po tytule
    FldShip := Pdf.NewFormField('ShippingAddress', 1);
    Pdf.SetFormFieldBounds(FldShip, 40, 120, 240, 20);
    Pdf.SetFormFieldValue(FldShip, '');

    // Połącz łącze Hide i łącze nawigacyjne z tą stroną
    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

    // Skrypt poziomu dokumentu dostępny dla każdego zdarzenia w pliku
    Pdf.AddGlobalJavaScript('OnOpen',
      'app.alert("Form ready", 3);');

    // Zamroź pole, jeśli wynik nie powinien być już edytowalny
    // Pdf.FlattenFormField(FldShip);

    if Pdf.SaveToFile('form_actions.pdf') <> 1 then
      raise Exception.Create('Save failed');
  finally
    Pdf.Free;
  end;
end;

Wywołanie flatten jest celowo zakomentowane. Pominięcie go sprawia, że dokument jest wysyłany jako aktywny formularz, którego akcje są uruchamiane w czytniku. Włączenie go powoduje wyrenderowanie pola do postaci statycznych znaków, co jest pożądane, gdy formularz został wypełniony, a wynik ma być przesyłany jako niezmienna instancja. To samo pole, ten sam kod, ale dwa skrajnie różne dokumenty w zależności od tego, czy go zamrozisz

Wybór właściwej operacji

Cztery opisywane akcje dzielą się wyraźnie ze względu na to, na co wpływają. Nazwana akcja przesuwa obszar roboczy przeglądarki i nie wymaga żadnego pola. Akcja Hide zmienia widoczność i wymaga nazw pól, przy czym kodowanie ciąg-kontra-tablica jest obsługiwane automatycznie. Akcja importu danych sięga do pliku na dysku, stąd jest niedozwolona w PDF/A. Akcja JavaScript uruchamia dowolną logikę i najlepiej podzielić ją na globalny pakiet funkcji oraz małe wywołania powiązane z akcjami. Sięgaj po najprostsze rozwiązanie realizujące dane zadanie: akcja Hide jest bardziej przenośna niż skrypt ustawiający flagę ukrycia, a nazwana akcja jest trwalsza niż zapisany cel strony, ponieważ nie wymaga utrzymywania konkretnego numeru

W tym miejscu obrazu dopełniają dwa powiązane tematy. Jeśli formularz jest częścią dokumentu o wysokiej dostępności, drzewo struktury, po którym poruszają się czytniki ekranu, zostało omówione w naszym artykule na temat struktury i dostępności znaczników PDF (tagged PDF). Gdy wypełniony formularz musi zostać zablokowany i podpisany, procedurę tę opisuje przewodnik po warsztacie zgodności i podpisywania. Wszystkie te trzy mechanizmy bazują na tym samym silniku, który jest dostarczany jako biblioteka PDF dla Delphi wraz z interfejsami API do tworzenia, obsługi formularzy oraz podpisów omówionymi w innych miejscach na tym blogu