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 PDFlibPas łą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.
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 PDFlibPas 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
// 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
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ć. PDFlibPas 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.
// 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);
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.
PDFlibPas 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.
// 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);
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;
// 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;
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.