Artykuł techniczny

Spłaszczanie hiperłączy XFA rich-text do linków PDF w Delphi

XFA, XML Forms Architecture, jest przestarzałe. ISO 32000-1 zawiera je w §12.7 z adnotacją, że zostało usunięte z PDF 2.0, a nowoczesne przeglądarki jedna po drugiej rezygnują ze swoich silników XFA. Nic z tego nie opróżniło archiwów. Rządowe formularze, wnioski ubezpieczeniowe i zestawienia bankowe były tworzone jako XFA przez blisko dwie dekady, a te pliki nadal trafiają do skrzynek pocztowych i potoków dokumentów. Gdy przeglądarka, która kiedyś je renderowała, przestaje to robić, formularz zamienia się w pustą stronę z komunikatem "proszę otworzyć w innym czytniku". Trwałą poprawką jest spłaszczenie XFA do statycznej treści PDF, którą każdy czytnik potrafi wyrenderować

Trudną częścią spłaszczania nie są pola. Pola tekstowe i pola wyboru dość sprawnie mapują się na widżety AcroForm. Trudną częścią jest tekst sformatowany, który XFA przechowuje wewnątrz elementu draw, w bloku <exData contentType="text/html">. Ten blok to podzbiór HTML z wbudowanym stylem i często kotwicami. Przeniesienie go na stronę oznacza odtworzenie zarówno tekstu ze stylem, jak i aktywnych hiperłączy - a hiperłącza są miejscem, gdzie większość implementacji cicho się poddaje

Jak wygląda rich text XFA w rzeczywistości

Ciało exData to mały wycinek XHTML. Akapit to <p>; stylizowany fragment znaków to <span> z własnym wbudowanym CSS dla grubości, pochylenia, koloru i rozmiaru; a hiperłącze to <a href="..."> owijające swój widoczny tekst. Jedna linia może zawierać kilka spanów z rzędu, każdy z innym stylem, a jeden z nich może być kotwicą. Styl nie jest dekoracją, którą można pominąć. Klauzula wyrenderowana pogrubioną czerwienią, bo jest ostrzeżeniem prawnym, musi pozostać pogrubiona i czerwona po spłaszczeniu, inaczej spłaszczony dokument fałszywie przedstawia oryginał

Silnik spłaszczający nie może traktować bloku jako jednego ciągu. Musi przejść przez strukturę inline, rozwiązać efektywny styl każdego fragmentu nakładając wbudowany CSS spana na bazową czcionkę elementu draw, i układać fragmenty jeden za drugim wzdłuż linii. HotPDF modeluje każdy z tych ułożonych fragmentów jako wewnętrzny rekord TXFARichRun. Rekord niesie tekst fragmentu, jego rozwiązany styl, jego zmierzony prostokąt i - dla kotwicy - Href, na który wskazuje

Układ fragmentów od lewej do prawej

Pozycjonowanie to miejsce, gdzie rich text przestaje być problemem parsowania i staje się problemem składu tekstu. Fragmenty współdzielą linię, więc każdy zaczyna się tam, gdzie skończył poprzedni. Nie ma znaczników rejestrujących tych pozycji - muszą być zmierzone. Wewnętrzna procedura silnika LayoutRichText mierzy każdy fragment tymi samymi metrykami czcionki, którymi go potem maluje, a następnie ustawia poziome przesunięcie fragmentu na sumę bieżącą wszystkich szerokości poprzednich fragmentów. Fragment pierwszy zaczyna się w początku ramki draw, fragment drugi - przy szerokości pierwszego, trzeci - przy łącznej szerokości dwóch pierwszych, i tak dalej przez całą linię

Dlatego tak ważne jest dopasowanie czcionki do pomiaru. Przebieg układu mierzy odstępy; oddzielny przebieg renderowania rysuje glify. Jeśli te dwa przebiegi nie zgadzają się co do czcionki, prostokąty obliczone przez układ nie będą leżeć pod glifami narysowanymi przez renderer. HotPDF synchronizuje je, mapując rozwiązany styl każdego fragmentu na specyfikację czcionki - przez wewnętrzny helper RunStyleToFontSpec - który pasuje do domyślnych ustawień renderera: Arial 10 punktów. Zmierzony odstęp i narysowany tekst zgadzają się, a obliczony prostokąt fragmentu naprawdę pokrywa znaki widoczne dla czytelnika

// Konceptualny kształt jednego ułożonego fragmentu. Silnik buduje ich tablicę
// wewnętrznie; nie konstruujesz ich samodzielnie, ale pola wyjaśniają, jak
// prostokąt kliknięcia linku pochodzi z mierzanej geometrii, a nie z tekstu.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // lewy górny róg, względem początku ramki draw
    W, H   : Double;       // zmierzony prostokąt fragmentu (szerokość z przebiegu układu)
    Text   : AnsiString;   // widoczne znaki fragmentu
    Href   : AnsiString;   // cel URI dla fragmentu <a>, '' w przeciwnym razie
  end;

Od fragmentu kotwicy do adnotacji Link w PDF

Hiperłącze w gotowym PDF nie jest częścią treści strony. Jest oddzielnym obiektem - adnotacją Link - opisaną w ISO 32000-1 §12.5.6.5. Adnotacja ma /Rect definiujący klikalny prostokąt na stronie i akcję uruchamianą po kliknięciu prostokąta. Dla łącza zewnętrznego akcją jest akcja URI: /S /URI z adresem docelowym jako ciągiem /URI. Widoczny tekst poniżej to zwykła treść strony; adnotacja to niewidoczna strefa klikalna nałożona na nią

Ścieżka spłaszczania dokładnie podąża za tym modelem. Gdy fragment niesie Href, HotPDF najpierw rysuje tekst ze stylem, a następnie buduje adnotację Link nad prostokątem fragmentu. Publiczny punkt wejścia dla tej adnotacji to metoda strony AddURILink, tworząca obiekt /Type /Annot /Subtype /Link z akcją /URI i zwracająca słownik adnotacji. Jej prostokąt to zmierzony prostokąt fragmentu, przetłumaczony z lokalnych współrzędnych elementu draw na współrzędne strony. Rezultatem jest łącze lądujące dokładnie na tekście kotwicy i nigdzie indziej

// Ten sam publiczny API, którego używa ścieżka spłaszczania dla każdego fragmentu kotwicy.
// Tworzy adnotację Link wg ISO 32000-1 12.5.6.5: /Subtype /Link z akcją /URI
// nad podanym prostokątem. Opcjonalny opis wypełnia /Contents dla czytnika ekranu.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // prostokąt kliknięcia w przestrzeni strony
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'Złóż odwołanie online');
end;

Dlaczego prostokąt kliknięcia musi pochodzić z mierzonych szerokości

Kuszące jest wyobrażenie sobie lokalizowania łącza przez wyszukanie jego widocznego tekstu na stronie i narysowanie prostokąta wokół tego, co zostanie znalezione. To nie działa, a przyczyna jest fundamentalna dla sposobu przechowywania spłaszczonego tekstu. Stylizowane fragmenty są malowane przy użyciu osadzonych czcionek podzbioru. Czcionka podzbioru przenumerowuje przechowywane glify, więc strumień treści strony zawiera szesnastkowe kody CID, a nie oryginalne kody znaków. Bajty na stronie to nie litery, które czyta człowiek, i nie można ich przeszukiwać jako tekstu. Wyszukiwanie podpisu kotwicy nic nie znajdzie, bo ten podpis nie istnieje jako literalny tekst nigdzie w strumieniu

Jedyną niezawodną kotwicą dla prostokąta jest geometria już wytworzona przez przebieg układu. Przesunięcie i zmierzona szerokość każdego fragmentu zostały obliczone podczas układania linii, przed przenumerowaniem jakiegokolwiek glifu, i opisują, gdzie tekst fizycznie się pojawi. HotPDF pobiera zatem prostokąt łącza bezpośrednio z ułożonego prostokąta fragmentu, a nie z jakiegokolwiek wyszukiwania tekstu. Ponieważ pomiar używał czcionki renderującej, prostokąt jest poprawny niezależnie od podzbioru. Geometria przeżywa kodowanie; tekst nie. To cały argument za pozycjonowaniem przez mierzone szerokości, i dlatego spłaszczacz próbujący dodać łącza wstecznie przez wyszukiwanie tekstu produkuje strefy klikalne, które dryfują lub znikają

Wywołanie spłaszczania z kodu

Dla PDF zawierającego już pakiet XFA punktem wejścia jest FlattenLoadedXFA. Załaduj dokument, wywołaj metodę i zapisz wynik. Parametr Editable decyduje, co dzieje się z polami formularza: przekaż True, by zachować je jako wypełnialne widżety AcroForm, albo False, by każdy widżet oznaczyć jako tylko do odczytu i uzyskać zamrożony zapis. Bloki draw rich-text ze swoimi stylizowanymi fragmentami i adnotacjami łączy są produkowane w obu przypadkach. Funkcja zwraca liczbę wyemitowanych widżetów

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_formularz.pdf');
    // True zachowuje pola wypełnialne; False zamraża je tylko do odczytu.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Wszystko, czego silnik nie mógł zmapować, jest zgłaszane, nie rzucane.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('Ostrzeżenie XFA: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('formularz_splaszczony.pdf');
    Writeln('Wyemitowane widżety: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

Zawsze czytaj XFAFlattenWarnings po wywołaniu. Lista jest czyszczona na początku każdego spłaszczania i gromadzi linię dla każdego elementu, którego silnik odmówił wyrenderować: nieobsługiwany rodzaj pola, obraz draw, który nie dał się zdekodować, blok exData bez użytecznych fragmentów. Żaden z nich nie rzuca wyjątku, więc pusta lista ostrzeżeń jest dowodem, że wszystko zostało zmapowane, a niepusta mówi dokładnie, które oryginały należy zbadać. Gdy posiadasz surowe dane XFA jako bajty XDP zamiast załadowanego PDF, bliźniacza metoda ApplyXFAAsAcroForm przyjmuje te bajty bezpośrednio i używa tej samej ścieżki kodu z tym samym zachowaniem ostrzeżeń. Uzupełniająca metoda AddXFAPacket działa w odwrotnym kierunku, osadzając pakiet XFA w budowanym dokumencie

Weryfikacja wyniku w przeglądarce

Otwórz spłaszczony plik w Acrobat lub dowolnej aktualnej przeglądarce i sprawdź dwie rzeczy. Po pierwsze, czy rich text wyrenderował się z zachowanym stylem: pogrubione fragmenty są pogrubione, kolorowe niosą swój kolor, a spany siedzą we właściwej kolejności na linii zamiast nachodzić na siebie lub wypadać poza ramkę. Po drugie, czy hiperłącza są aktywne. Najedź na kotwicę i pasek stanu powinien pokazywać adres docelowy; kliknij, a akcja URI powinna go otworzyć. Użyj inspektora adnotacji przeglądarki, by potwierdzić, że każda to prawdziwa adnotacja /Link, której /Rect przylega do tekstu kotwicy, leżąc nad treścią, która jest teraz zwykłymi namalowanymi glifami zamiast renderowanego przez XFA formularza. Ta kombinacja - stylizowany tekst statyczny plus prawdziwe adnotacje Link na właściwych prostokątach - sprawia, że spłaszczony dokument przeżyje silniki XFA, których już nie potrzebuje

Spłaszczanie samych pól - pól tekstowych, pól wyboru i list rozwijanych otaczających ten rich text - jest omówione w artykule o spłaszczaniu formularzy XFA do widżetów AcroForm. Dla szerszego obrazu budowania i umieszczania adnotacji Link ręcznie, poza tymi generowanymi przez ścieżkę spłaszczania, zobacz artykuł o pracy z adnotacjami PDF w HotPDF. Oba opierają się na tym samym modelu adnotacji i formularzy dostarczanym przez komponent HotPDF dla Delphi i C++Builder