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
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
Reszta mapowania UI idzie za tabelami flag pól z ISO 32000-1 §12.7.4:
textEdit,numericEditidateTimeEditstają się/FT /Tx; wieloliniowe pole tekstowe dostaje flag Multiline (/Ff 4096, tabela 228) i strumień wyglądu dzielony na liniepasswordEditstaje się/FT /Txz flagiem Password (/Ff 8192) i wyglądem rysującym jedną gwiazdkę na skalar Unicode, nie na jednostkę kodową UTF-16checkButtonstaje się polem wyboru/FT /Btn, którego wartość to/Yes, gdy związane dane to1, i/Offinaczej<exclGroup>staje się jednym rodzicem radio/FT /Btn /Ff 32768(tabela 226) z/V,/DV,/Opti/Kids, plus jeden widget dziecka na element z własnym/ASi dwustanowym/APimageEditstaje się pushbuttonem, którego wygląd maluje obraz; baseline JPEG jest wbudowywany dosłownie pod/DCTDecode, a PNG jest dekodowany i rekodowany jako piksele/FlateDecodez 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
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