Technický článek

Konverze statických formulářů XFA na pole AcroForm v Delphi

PDFium Component převádí statické formuláře XFA na obyčejné pole AcroForm z Delphi přes FlattenPdfXfaToAcroForm v unitu FPdfDocumentXfaFlatten. Funkce čte packety šablony a datasets, mapuje každé absolutně pozicované pole na terminální pole AcroForm s vlastním appearance streamem a připojuje výsledek jako inkrementální revize, takže převedené PDF se otevírá jako normální formulář v prohlížečích, které XFA nikdy nepodporovaly

Motivací bývá obvykle support ticket, ne položka plánu. Vládní nebo pojišťovací formulář vznikl v designeru, který ho uložil jako XFA, funguje v jednom desktopovém čteči a všude jinde se otevírá jako prázdná stránka, která říká, že dokument vyžaduje novější prohlížeč. XFA bylo v PDF 2.0 (ISO 32000-2) zavrženo, prohlížeče a mobilní čteči ho nikdy neimplementovaly a samo PDFium renderuje XFA jen s buildem zapnutým na V8. Statické formuláře XFA, tedy ty, jejichž layout je pevný už v době návrhu, nesou v šabloně dost geometrie na to, aby se znovu postavily jako pole AcroForm, a přesně ten slice tahle konverze obsluhuje

Co FlattenPdfXfaToAcroForm doopravdy produkuje?

FlattenPdfXfaToAcroForm produkuje revizi AcroForm na úrovni bajtů připojenou k nezměněné kopii zdroje, nikdy přepis původního souboru. PDFium nevystavuje žádné veřejné API pro vytváření formulářových polí, takže komponenta na tuhle práci nechodí přes PDFium vůbec: čte aktivní revizi vlastním čtečem objektů, staví slovníky polí, widget anotace, appearance XObjects a fontový zdroj /Helv jako syrové PDF objekty a serializuje je přes řídký inkrementální update ve smyslu ISO 32000-1 §7.5.6

Nový slovník /AcroForm nese /Fields, /DA a /DR a žádnou položku /XFA. Drží-li zdroj své AcroForm jako indirect objekt, ten se přepíše na místě; inline slovník se vymění uvnitř katalogu. Tak jako tak už aktivní revize nemíří na packety XFA, takže prohlížeč, který rozumí oběma modelům, otevře pole AcroForm místo pokusu o layout formuláře XFA. Původní bajty XFA jsou stále v dřívější revizi, což je to, co inkrementální update znamená, ale nic v aktuální revizi na ně neodkazuje

Pipeline konverze statického XFA na AcroForm v PDFium Component pro Delphi: FlattenPdfXfaToAcroForm čte packety šablony a datasets, mapuje absolutně pozicovaná pole na slovníky polí a widgety a připojuje AcroForm jako řídkou inkrementální revizi bez položky /XFA
FlattenPdfXfaToAcroForm nikdy nesáhne na původní bajty: převedená pole, widgety i appearance streamy se připojí jako revize, jejíž /AcroForm prostě přestane mířit na packety 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;   // default je 512, povolený rozsah je 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;

Funkce vrací False a plní Report.ErrorText pro jakýkoli odmítnutý nebo poškozený vstup a na výstupu obnoví pozici zdrojového streamu. Zrušení je jediná cesta výjimky, která se šíří: vzbudí-li se Options.CancellationToken, znovu se vyhodí EPdfOperationCancelled poté, co se zaplní report. TPdfXfaFlattenReport taky nese SourceSha256 a OutputSha256, což se hodí, když dávková úloha musí dokázat, který vstup vyprodukoval který převedený soubor. TPdfXfaFlattenOptions.Default nastavuje rozpočet packetů 8 MiB, 512 polí a strop výstupu 512 MiB; zdroj nesmí být šifrovaný

Čtení packetů šablony a datasets

PDFium Component přijímá oba layouty, které ISO 32000-1 §12.7.8 dovoluje pro položku /XFA: formu pole, která střídá názvy packetů a reference streamů, a formu XDP jediného streamu, kde jeden stream drží celý dokument <xdp:xdp>. Streamy packetů mohou být nekomprimované nebo /FlateDecode bez prediktoru a dekomprese se ohraničuje přes MaxPacketBytes

Případ jediného streamu je místo, kde naivní obsluha XML selhává. Designéři běžně píší <xfa:datasets> s prefixem namespace, zatímco šablona sedí bez prefixu, takže krájení podle literálního názvu tagu najde jeden packet a druhý minout. Konverze porovnává názvy elementů a atributů podle lokálního názvu, po samostatné kontrole, že se otevírací a zavírací tagy párují podle svého plného s prefixem jména. Potřebujete-li se jen podívat na packety místo konverze, průvodce detekcí formulářů XFA a extrakcí jejich packetů s PDFium VCL pokrývá tu read-only cestu

Hodnoty polí přicházejí ze dvou míst. Element <value> v šabloně dodává výchozí hodnotu a packet datasets dodává vyplněná data, vázaná podle názvu pole. Názvy polí se v reálných formulářích opakují, takže binder srovnává řetěz pojmenovaných předků <subform> s cestou předků v datech a bere nejdéle nekonfliktní shodu. Když sdílejí dva listová pole název pod různými subformami, stane se AcroForm /T SubformPath.Name, takže dvě pole zůstanou po konverzi odlišná

Proč potřebuje choiceList explicitní flag Combo?

XFA a PDF mají opačné defaulty u polí výběru, takže konverze, která nekopíruje žádné flagy, změní každý drop-down na list box. XFA <choiceList> s defaultním open="userControl" nebo s open="onEntry" je drop-down; PDF pole /FT /Ch bez flagů je scrollovací list box a jen flag Combo (bitová pozice 18 v tabulce 230 ISO 32000-1, hodnota 131072, tentýž bit, který PDFium jmenuje FPDF_FORMFLAG_CHOICE_COMBO) z něj udělá combo box. Konverze proto nastavuje flag Combo pro případy drop-down a nechává flagy prázdné jen tehdy, když šablona říká open="always" nebo open="multiSelect", které drží seznam trvale otevřený. Buildy před 3.181.1 prohodily hodnoty Combo a Pushbutton a hledaly nestandardní open="on", takže formuláře jimi zploštělé stojí za opětovný běh

Rozhodnutí o flagu pole výběru v konverzi statického XFA na AcroForm v PDFium Component: atribut open userControl nebo onEntry nastaví flag Combo, bit 18 hodnota 131072, který vytvoří drop-down combo box, zatímco open always nebo multiSelect nechá flagy prázdné pro trvale otevřený list box
Bez flagu Combo se XFA drop-down zploští na scrollovací list box, takže konverze čte atribut open a nastavuje bit 18 jen pro případy drop-down

Zbytek mapování UI následuje tabulky flagů polí v ISO 32000-1 §12.7.4:

  • textEdit, numericEdit a dateTimeEdit se stanou /FT /Tx; víceřádkové textové pole dostane flag Multiline (/Ff 4096, tabulka 228) a appearance stream dělený na řádky
  • passwordEdit se stane /FT /Tx s flagem Password (/Ff 8192) a appearance, která kreslí jednu hvězdičku na Unicode skalár, ne na UTF-16 code unit
  • checkButton se stane zaškrtávacím boxem /FT /Btn, jehož hodnota je /Yes, když jsou vázaná data 1, a /Off jinak
  • <exclGroup> se stane jedním rodičem radio /FT /Btn /Ff 32768 (tabulka 226) s /V, /DV, /Opt a /Kids, plus jedním dětským widgetem na člena s vlastním /AS a dvoustavovým /AP
  • imageEdit se stane pushbuttonem, jehož appearance maluje obrázek; baseline JPEG se embedduje slovo od slova pod /DCTDecode, zatímco PNG se dekóduje a znovu zakóduje jako pixely /FlateDecode s extra /SMask, když PNG nese alfu nebo průhlednost tRNS

Hodnoty, které nejsou prostě tisknutelné ASCII, včetně zlomů řádku, se zapisují jako řetězce <FEFF...> UTF-16BE, takže japonské jméno nebo dvouřádková adresa cestu přežije. Výstup Fields zrcadlí, co se zapsalo, což z něj dělá pohodlnou auditní stopu

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;

Geometrie, měrné jednotky a směrování stránek

Geometrie XFA se převádí do PDF user space překlopením každého pole proti výšce MediaBox jeho vlastní stránky, protože XFA měří shora dolů od vršku stránky a PDF měří odspodu vzhůru. Posuny z vnořených elementů <subform> se akumulují při sestupu šablonou a každé pole musí skončit s kladnou šířkou i výškou, jinak se konverze odmítne

Konverze geometrie XFA na PDF v PDFium Component pro Delphi: obdélník pole měřený shora dolů od horní hrany v šabloně XFA se překlopí proti výšce MediaBox jeho vlastní stránky do PDF user space, kde souřadnice y roste odspodu vzhůru
Sestupná šablona XFA a vzestupný PDF user space se potkají v jednom odečtení proti výšce MediaBox, které umístí každý převedený widget na jeho navrženou pozici

Měření přijímají jednotky, které skutečné designéři vysílají: holá čísla a pt, plus in (72 bodů), mm (72 / 25,4), cm (72 / 2,54), pc (12), px (0,75) a mp (0,001, milipointy). Parsování je nezávislé na locale, takže německá instalace Windows nezmění 8.5in v nesmysl. Neznámá jednotková přípona se odmítne místo čtení jako body, což by pole tiše přemístilo

Směrování stránek následuje <pageSet>: sourozenecké elementy <pageArea> se číslují od jedničky v pořadí deklarací a přisvojí si pole vnořená pod nimi, zatímco pole bez předka <pageArea> zůstávají na stránce jedna. Každá stránka dostane vlastní update /Annots. Pole směrované za poslední stránku PDF je chyba, ne widget shozený na stránku jedna

Které formuláře XFA konverze odmítá?

Konverze odmítá cokoli, jehož layout nelze znát bez pustění XFA layout enginu. To znamená atribut layout jiný než position na <subform> nebo <exclGroup> (protékané tb, lr-tb a spol., z čehož se staví dynamické formuláře), jakýkoli element <script> a jakýkoli druh UI mimo sedm uvedených výše. Dynamické formuláře, které rostou stránkami za běhu, patří do jiné sady nástrojů; článek o dynamické repaginaci XFA a počtech stránek v PDFium Component ukazuje, co ten živý layout obnáší

Odmítnutí je záměr. Protékaný subform nemá žádné pevné souřadnice, takže jakákoli „konverze" by musela pozice vymyslet a skriptovaný formulář často počítá součty nebo přepíná viditelnost, což statický AcroForm nedokáže reprodukovat. Vytvořit PDF, které vypadá kompletně, ale tiše ztratilo spočtený součet, je horší než jasná chyba v Report.ErrorText. Specifikace XFA 3.3 definuje mnohem víc než tato podmnožina a konverze se nedělá, že ne

Pasti při zápisu a ověřování revize AcroForm

Při stavbě writeru se ukázaly dva problémy a oba se týkají každého, kdo produkuje inkrementální updaty ručně. První: katalog musí být vždycky součástí update sady, i když se jeho bajty nemění, protože AcroForm je indirect objekt přepsaný jinde. Řídký update writer odvozuje trailer /Root z objektů, které nese, a update bez kořenového objektu produkuje revizi, kterou prohlížeče nemohou ukotvit

Druhý se týká ověřování. Načtení převedeného souboru přes wrapper PDFium spustí inicializaci formulářového prostředí, jakmile má dokument /AcroForm, a hostitelské pdfium.dll v testovacím uspořádání postrádalo export FORM_GetTextDirection, takže inicializace selhala dřív, než se stihla zkontrolovat jediná pole. Kontroly čtení zpět proto používají čteč objektů komponenty, který parsuje aktivní revizi bez dotknutí formulářové vrstvy

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);
    // aktivní katalog musí odkazovat na AcroForm; inline AcroForm
    // zapsaný konverzí nenese žádnou položku /XFA
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Appearance streamy jsou záměrně prosté: Helvetica v 10 bodech pro text a hodnoty výběru, šrafovaný čtverec pro zaškrtávací boxy, vyplněný vnitřní čtverec pro vybraný radio stav. Učiní převedený formulář čitelným všude, ale nejsou pixelovou kopií původního XFA renderu, takže fonty, barvy a okraje definované v šabloně se nepřenesou. Musí-li formulář vypadat identicky s výstupem designéra, vyrenderujte ho jednou XFA buildem a srovnejte, než přepnete produkční pipeline

FAQ

Potřebuje FlattenPdfXfaToAcroForm V8 nebo XFA build PDFium?

Ne. FlattenPdfXfaToAcroForm parsuje packety a zapisuje revizi kódem PDFium Component, takže funguje se standardním pdfium.dll a neprovádí žádný XFA skript

Zvládne konverze šifrované dokumenty XFA?

Ne přímo. Šifrovaný zdroj se odmítne s chybou, takže dokument nejdřív dešifrujte a převeďte dešifrovanou kopii

Jsou převedená pole stále editovatelná?

Ano. Výstup obsahuje skutečná textová pole AcroForm, zaškrtávací boxy, radio, pole výběru a pushbuttony s hodnotami i appearance streamy, takže uživatelé mohou formulář dál vyplňovat v jakémkoli AcroForm schopném prohlížeči

Konverze statického XFA, extrakce packetů XFA i XFA runtime zapnutý na V8 jsou všechny součástí PDFium Component for Delphi and C++Builder, který obaluje PDFium pro projekty v Delphi, C++Builderu a Lazarusu