Artykuł techniczny

Statyczne formularze XFA na pola AcroForm w Delphi

PDFium Component konwertuje statyczne formularze XFA na zwykłe pola AcroForm z Delphi przez FlattenPdfXfaToAcroForm z modułu FPdfDocumentXfaFlatten. Funkcja czyta pakiety szablonu i datasets XFA, mapuje każde absolutnie pozycjonowane pole na terminalne pole AcroForm z własnym strumieniem wyglądu i dokleja wynik jako rewizję przyrostową, więc skonwertowany PDF otwiera się jako zwykły formularz w przeglądarkach, które nigdy nie wspierały XFA

Motywacją jest zwykle zgłoszenie supportowe, a nie punkt roadmapy. Formularz rządowy albo ubezpieczeniowy powstał w designerze, który zapisał go jako XFA, działa w jednym desktopowym czytniku, a wszędzie indziej otwiera się jako pusta strona mówiąca, że dokument wymaga nowszego czytnika. XFA zostało uznane za przestarzałe w PDF 2.0 (ISO 32000-2), przeglądarki i czytniki mobilne nigdy go nie zaimplementowały, a sam PDFium renderuje XFA tylko w budowie z włączonym V8. Statyczne formularze XFA, te o układzie ustalonym w czasie projektowania, niosą w szablonie dość geometrii, żeby zbudować je na nowo jako pola AcroForm, i dokładnie ten wycinek obsługuje ten konwerter

Co FlattenPdfXfaToAcroForm faktycznie produkuje?

FlattenPdfXfaToAcroForm produkuje rewizję AcroForm na poziomie bajtów, doklejaną do niezmienionej kopii źródła, nigdy przepisanie oryginalnego pliku. PDFium nie wystawia publicznego API do tworzenia pól formularzy, więc komponent w ogóle nie idzie w tej robocie przez PDFium: czyta aktywną rewizję własnym czytnikiem obiektów, buduje słowniki pól, adnotacje widgetów, XObjects wyglądu i zasób fontu /Helv jako surowe obiekty PDF i serializuje je przez rzadką aktualizację przyrostową w sensie ISO 32000-1 §7.5.6

Nowy słownik /AcroForm niesie /Fields, /DA i /DR i żaden wpis /XFA. Gdy źródło trzyma swoje AcroForm jako obiekt pośredni, ten obiekt jest przepisywany na miejscu; słownik inline jest wymieniany wewnątrz katalogu. Tak czy inaczej aktywna rewizja przestaje wskazywać pakiety XFA, więc przeglądarka rozumiejąca oba modele otwiera pola AcroForm zamiast próbować łamać formularz XFA. Pierwotne bajty XFA są wciąż w wcześniejszej rewizji, bo o tym jest aktualizacja przyrostowa, ale nic w bieżącej rewizji ich nie referuje

Potok konwersji statycznych XFA na AcroForm w PDFium Component dla Delphi: FlattenPdfXfaToAcroForm czyta pakiety szablonu i datasets, mapuje absolutnie pozycjonowane pola na słowniki pól i widgety i dokleja AcroForm jako rzadką rewizję przyrostową bez wpisu /XFA
FlattenPdfXfaToAcroForm nigdy nie dotyka pierwotnych bajtów: skonwertowane pola, widgety i strumienie wyglądu są doklejane jako rewizja, której /AcroForm po prostu przestaje wskazywać pakiety XFA
uses
  System.SysUtils, System.Classes, System.IOUtils, FPdfDocumentXfaFlatten;

procedure ConvertXfaForm(const SourceFile, TargetFile: string);
var
  Source: TFileStream;
  Options: TPdfXfaFlattenOptions;
  OutputPdf: TBytes;
  Report: TPdfXfaFlattenReport;
  Fields: TPdfXfaFlattenFields;
begin
  Options := TPdfXfaFlattenOptions.Default;
  Options.MaxFields := 1024;   // domyślnie 512, dozwolony zakres to 1..4096

  Source := TFileStream.Create(SourceFile, fmOpenRead or fmShareDenyWrite);
  try
    if not FlattenPdfXfaToAcroForm(Source, Options, OutputPdf, Report, Fields) then
      raise Exception.Create('XFA conversion failed: ' + Report.ErrorText);
  finally
    Source.Free;
  end;

  TFile.WriteAllBytes(TargetFile, OutputPdf);
  Writeln(Format('%d template fields, %d bound to datasets, %d written on %d pages',
    [Report.TemplateFields, Report.DatasetBound, Report.FieldsWritten, Report.PagesWritten]));
end;

Funkcja zwraca False i wypełnia Report.ErrorText dla każdego odrzuconego albo zniekształconego wejścia i przywraca pozycję strumienia źródłowego na wyjściu. Anulowanie to jedyna ścieżka wyjątku, która się propaguje: gdy Options.CancellationToken odpala, EPdfOperationCancelled jest podnoszone ponownie po wypełnieniu raportu. TPdfXfaFlattenReport niesie też SourceSha256 i OutputSha256, co jest poręczne, gdy zadanie wsadowe musi udowodnić, które wejście wyprodukowało który skonwertowany plik. TPdfXfaFlattenOptions.Default ustawia budżet pakietów 8 MiB, 512 pól i sufit wyjścia 512 MiB; źródło nie może być zaszyfrowane

Czytanie pakietów szablonu i datasets

PDFium Component przyjmuje oba układy, jakie ISO 32000-1 §12.7.8 pozwala dla wpisu /XFA: formę tablicową przeplatającą nazwy pakietów i referencje strumieni oraz formę XDP pojedynczego strumienia, gdzie jeden strumień trzyma cały dokument <xdp:xdp>. Strumienie pakietów mogą być nieskompresowane albo /FlateDecode bez predyktora, a dekompresja jest ograniczona przez MaxPacketBytes

Przypadek pojedynczego strumienia to miejsce, gdzie naiwna obsługa XML się łamie. Designery piszą zwykle <xfa:datasets> z prefiksem przestrzeni nazw, podczas gdy szablon siedzi bez prefiksu, więc cięcie po literalnej nazwie taga znajduje jeden pakiet i gubi drugi. Konwerter dopasowuje nazwy elementów i atrybutów po nazwie lokalnej, po osobnym sprawdzeniu, że tagi otwierające i zamykające parują się po pełnej nazwie z prefiksem. Jeśli potrzebujesz tylko obejrzeć pakiety, a nie ich konwertować, przewodnik po wykrywaniu formularzy XFA i wyciąganiu ich pakietów z PDFium VCL opisze tę ścieżkę tylko-do-odczytu

Wartości pól pochodzą z dwóch miejsc. Element <value> w szablonie podaje domyślną, a pakiet datasets podaje wypełnione dane, wiązane nazwą pola. Nazwy pól powtarzają się w prawdziwych formularzach, więc binder porównuje łańcuch nazwanych przodków <subform> ze ścieżką przodków w danych i bierze najdłuższe niekolidujące dopasowanie. Gdy dwa pola liścia dzielą nazwę pod różnymi subformami, AcroForm /T staje się SubformPath.Name, żeby oba pola zostały odrębne po konwersji

Dlaczego choiceList potrzebuje jawnego flagu Combo?

XFA i PDF mają przeciwne domyślne dla pól wyboru, więc konwerter niekopiujący żadnych flag zamienia każdy dropdown w list box. XFA <choiceList> z domyślnym open="userControl" albo z open="onEntry" to dropdown; pole PDF /FT /Ch bez flag to przewijany list box i tylko flag Combo (pozycja bitu 18 w tabeli 230 ISO 32000-1, wartość 131072, ten sam bit, który PDFium nazywa FPDF_FORMFLAG_CHOICE_COMBO) czyni z niego combo box. Konwerter ustawia więc flag Combo dla przypadków dropdown i zostawia flagi puste tylko, gdy szablon mówi open="always" albo open="multiSelect", co trzyma listę stale otwartą. Budowy przed 3.181.1 zamieniały miejscami wartości Combo i Pushbutton i szukały niestandardowego open="on", więc formularze spłaszczone nimi warto przerobić ponownie

Decyzja o fladze pola wyboru w konwersji statycznych XFA na AcroForm w PDFium Component: atrybut open userControl albo onEntry ustawia flag Combo, bit 18 wartość 131072, produkując rozwijany combo box, podczas gdy open always albo multiSelect zostawia flagi puste dla stale otwartego list boxa
Bez flagu Combo XFA dropdown spłaszcza się w przewijany list box, więc konwerter czyta atrybut open i ustawia bit 18 tylko dla przypadków dropdown

Reszta mapowania UI idzie za tabelami flag pól z ISO 32000-1 §12.7.4:

  • textEdit, numericEdit i dateTimeEdit stają się /FT /Tx; wieloliniowe pole tekstowe dostaje flag Multiline (/Ff 4096, tabela 228) i strumień wyglądu dzielony na linie
  • passwordEdit staje się /FT /Tx z flagiem Password (/Ff 8192) i wyglądem rysującym jedną gwiazdkę na skalar Unicode, nie na jednostkę kodową UTF-16
  • checkButton staje się polem wyboru /FT /Btn, którego wartość to /Yes, gdy związane dane to 1, i /Off inaczej
  • <exclGroup> staje się jednym rodzicem radio /FT /Btn /Ff 32768 (tabela 226) z /V, /DV, /Opt i /Kids, plus jeden widget dziecka na element z własnym /AS i dwustanowym /AP
  • imageEdit staje się pushbuttonem, którego wygląd maluje obraz; baseline JPEG jest wbudowywany dosłownie pod /DCTDecode, a PNG jest dekodowany i rekodowany jako piksele /FlateDecode z dodatkowym /SMask, gdy PNG niesie alfę albo przezroczystość tRNS

Wartości, które nie są zwykłym drukowalnym ASCII, łącznie z końcami wierszy, są zapisywane jako napisy <FEFF...> UTF-16BE, więc japońskie nazwisko albo dwuwierszowy adres przeżywają podróż. Wyjście Fields zwierciadli to, co zostało zapisane, co czyni je poręcznym śladem audytowym

procedure DumpConvertedFields(const Fields: TPdfXfaFlattenFields);
const
  KindNames: array[TPdfXfaFlattenFieldKind] of string = (
    'text', 'check', 'choice', 'image', 'numeric', 'dateTime', 'password', 'radio');
var
  I: Integer;
  F: TPdfXfaFlattenField;
begin
  for I := 0 to High(Fields) do
  begin
    F := Fields[I];
    Writeln(Format('%s.%s [%s] page %d at %.1f,%.1f size %.1f x %.1f',
      [F.SubformPath, F.Name, KindNames[F.Kind], F.PageIndex + 1, F.X, F.Y, F.W, F.H]));
    case F.Kind of
      fxfkChoice:
        Writeln(Format('  %d items, list box: %s', [Length(F.Choices), BoolToStr(F.ListBox, True)]));
      fxfkRadio:
        Writeln(Format('  %d radio widgets, value "%s"', [Length(F.RadioWidgets), F.Value]));
      fxfkImage:
        Writeln(Format('  %d image bytes', [Length(F.ImageData)]));
    else
      Writeln('  value "' + F.Value + '"');
    end;
  end;
end;

Geometria, jednostki miary i trasowanie stron

Geometria XFA jest przeliczana na przestrzeń użytkownika PDF przez odwrócenie każdego pola względem wysokości MediaBox jego własnej strony, bo XFA mierzy w dół od góry strony, a PDF mierzy w górę od dołu. Przesunięcia z zagnieżdżonych elementów <subform> akumulują się w drodze w dół szablonu, a każde pole musi skończyć z dodatnią szerokością i wysokością, inaczej konwersja jest odrzucana

Konwersja geometrii XFA na PDF w PDFium Component dla Delphi: prostokąt pola mierzony w dół od górnej krawędzi w szablonie XFA jest odwracany względem wysokości MediaBox jego własnej strony do przestrzeni użytkownika PDF, gdzie współrzędna y rośnie w górę od dołu
W dół mierzony szablon XFA i w górę rosnąca przestrzeń użytkownika PDF spotykają się w jednym odejmowaniu względem wysokości MediaBox, co umieszcza każdy skonwertowany widget na jego zaprojektowanej pozycji

Miary przyjmują jednostki, jakie emitują prawdziwe designery: gołe liczby i pt, plus in (72 punkty), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) i mp (0.001, milipunkty). Parsowanie jest niezależne od locale, więc niemiecka instalacja Windows nie zamieni 8.5in w bezsens. Nieznany przyrostek jednostki jest odrzucany, zamiast być czytany jako punkty, co po cichu przestawiłoby pole

Trasowanie stron idzie za <pageSet>: elementy <pageArea> rodzeństwa są numerowane od jednego w kolejności deklaracji i przejmują pola zagnieżdżone pod nimi, podczas gdy pola bez przodka <pageArea> zostają na stronie pierwszej. Każda strona dostaje własną aktualizację /Annots. Pole skierowane ponad ostatnią stronę PDF to błąd, a nie widget zrzucony na stronę pierwszą

Które formularze XFA konwerter odrzuca?

Konwerter odrzuca wszystko, czego układu nie da się poznać bez uruchomienia silnika układu XFA. To znaczy atrybut layout inny niż position na <subform> albo <exclGroup> (płynne tb, lr-tb i przyjaciele, z czego zbudowane są formularze dynamiczne), każdy element <script> i każdy rodzaj UI poza siedmioma wypisanymi wyżej. Formularze dynamiczne rosnące o strony w czasie biegu należą do innego łańcucha narzędzi; artykuł o dynamicznej repaginacji XFA i liczbach stron w PDFium Component pokazuje, co niesie ten żywy układ

Odmowa jest celowa. Płynna subforma nie ma stałych współrzędnych, więc każda "konwersja" musiałaby wymyślać pozycje, a formularz ze skryptami często liczy sumy albo przełącza widoczność, czego statyczny AcroForm nie odtworzy. Wyprodukowanie PDF-a wyglądającego kompletnie, ale z po cichu zgubioną policzoną sumą, jest gorsze niż jasny błąd w Report.ErrorText. Specyfikacja XFA 3.3 definiuje daleko więcej niż ten podzbiór i konwerter nie udaje inaczej

Pułapki przy pisaniu i weryfikacji rewizji AcroForm

Dwa problemy wyszły przy budowie writera i oba dotyczą każdego produkującego aktualizacje przyrostowe ręcznie. Pierwszy: katalog musi zawsze być częścią zestawu aktualizacji, nawet gdy jego bajty się nie zmieniają, bo AcroForm jest obiektem pośrednim przepisywanym gdzie indziej. Writer aktualizacji rzadkiej wyprowadza trailer /Root z obiektów, które niesie, a aktualizacja bez obiektu korzenia produkuje rewizję, której przeglądarki nie umieją zakotwiczyć

Drugi dotyczy weryfikacji. Wczytanie skonwertowanego pliku przez owijkę PDFium odpala inicjalizację środowiska formularzy, jak tylko dokument ma /AcroForm, a gospodarujący pdfium.dll w zestawie testowym nie miał eksportu FORM_GetTextDirection, więc inicjalizacja zawodziła, zanim pojedyncze pole zdążyło zostać sprawdzone. Sprawdzenia odczytu używają więc czytnika obiektów komponentu, który parsuje aktywną rewizję bez dotykania warstwy formularzy

uses
  System.SysUtils, System.Classes, FPdfCompress;

function HasActiveAcroForm(const Pdf: TBytes): Boolean;
var
  Stream: TBytesStream;
  Objects: TPdfActiveObjects;
  Revision: TPdfSparseRevisionInfo;
  ErrorText: string;
begin
  Stream := TBytesStream.Create(Pdf);
  try
    if not ReadPdfActiveObjects(Stream, TPdfActiveObjectReadOptions.Default,
      Objects, Revision, ErrorText) then
      raise Exception.Create(ErrorText);
    // aktywny katalog musi referować AcroForm; inline AcroForm
    // zapisany przez konwerter nie niesie wpisu /XFA
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Strumienie wyglądu są celowo proste: Helvetica 10 punktów dla tekstu i wartości wyboru, kreskowany kwadrat dla pól wyboru, wypełniony wewnętrzny kwadrat dla zaznaczonego stanu radio. Czynią one skonwertowany formularz czytelnym wszędzie, ale nie są pikselom w piksel kopią pierwotnego renderowania XFA, więc fonty, kolory i ramki zdefiniowane w szablonie nie są przenoszone. Jeśli formularz ma wyglądać identycznie jak wyjście designera, zrenderuj go raz budową XFA i porównaj, zanim przełączysz produkcyjny potok

FAQ

Czy FlattenPdfXfaToAcroForm potrzebuje budowy V8 albo XFA PDFium?

Nie. FlattenPdfXfaToAcroForm parsuje pakiety i pisze rewizję kodem PDFium Component, więc działa ze standardowym pdfium.dll i nie wykonuje żadnego skryptu XFA

Czy konwerter ogarnie zaszyfrowane dokumenty XFA?

Nie bezpośrednio. Zaszyfrowane źródło jest odrzucane z błędem, więc odszyfruj najpierw dokument i skonwertuj odszyfrowaną kopię

Czy skonwertowane pola są wciąż edytowalne?

Tak. Wyjście zawiera prawdziwe pola AcroForm: tekstowe, pola wyboru, radio, wybór i pushbutton z wartościami i strumieniami wyglądu, więc użytkownicy mogą dalej wypełniać formularz w dowolnej przeglądarce wspierającej AcroForm

Konwersja statycznych XFA, wyciąganie pakietów XFA i runtime XFA z włączonym V8 to wszystko części PDFium Component for Delphi and C++Builder, które opakowuje PDFium dla projektów Delphi, C++Builder i Lazarus