Technisch artikel

Statische XFA-formulieren naar AcroForm-velden in Delphi

PDFium Component zet statische XFA-formulieren om in gewone AcroForm-velden vanuit Delphi via FlattenPdfXfaToAcroForm in de unit FPdfDocumentXfaFlatten. De functie leest de XFA-template- en datasets-packets, mapt elk absoluut gepositioneerd veld op een AcroForm-terminalveld met zijn eigen appearance stream, en voegt het resultaat toe als incrementele revisie, zodat de omgezette PDF opent als een normaal formulier in viewers die XFA nooit hebben ondersteund

De motivatie is doorgaans een supportticket in plaats van een roadmapitem. Een overheids- of verzekeringsformulier is vervaardigd in een designer die hem als XFA opslag, hij werkt in één desktopreader, en overal elders opent hij als een lege pagina die zegt dat het document een nieuwere viewer vereist. XFA is deprecated in PDF 2.0 (ISO 32000-2), browsers en mobiele viewers hebben hem nooit geïmplementeerd, en PDFium zelf rendert XFA alleen met de build met V8. Statische XFA-formulieren, degenen waarvan de lay-out op designtijd vaststaat, dragen genoeg geometrie in de template om te worden herbouwd als AcroForm-velden, en precies die plak behandelt deze converter

Wat levert FlattenPdfXfaToAcroForm werkelijk op?

FlattenPdfXfaToAcroForm levert een AcroForm-revisie op byteniveau op, toegevoegd aan een onveranderde kopie van de bron, nooit een herschrijving van het originele bestand. PDFium stelt geen publieke API bloot voor het maken van formuliervelden, dus de component loopt voor deze klus helemaal niet via PDFium: hij leest de actieve revisie met zijn eigen objectreader, bouwt velddictionaries, widgetannotaties, appearance XObjects en een fontresource /Helv als rauwe PDF-objecten, en serialiseert ze via een sparse incrementele update in de zin van ISO 32000-1 §7.5.6

De nieuwe dictionary /AcroForm draagt /Fields, /DA en /DR en geen entry /XFA. Houdt de bron zijn AcroForm als indirect object vast, dan wordt dat object op zijn plek herschreven; een inline dictionary wordt vervangen binnen de catalogus. Hoe dan ook wijst de actieve revisie niet meer naar de XFA-packets, dus een viewer die beide modellen begrijpt opent de AcroForm-velden in plaats van te proberen de XFA-vorm uit te zetten. De originele XFA-bytes zitten nog in de eerdere revisie, wat een incrementele update nu eenmaal betekent, maar niets in de huidige revisie verwijst ernaar

Conversiepijplijn van statische XFA naar AcroForm in PDFium Component voor Delphi: FlattenPdfXfaToAcroForm leest de template- en datasets-packets, mapt absoluut gepositioneerde velden op velddictionaries en widgets, en voegt de AcroForm toe als sparse incrementele revisie zonder een /XFA-entry
FlattenPdfXfaToAcroForm raakt de originele bytes nooit aan: de omgezette velden, widgets en appearance streams worden toegevoegd als een revisie waarvan /AcroForm simpelweg stopt met naar de XFA-packets wijzen
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 is 512, het toegestane bereik is 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;

De functie geeft False terug en vult Report.ErrorText voor elke geweigerde of misvormde invoer, en hij herstelt de bronstreampositie bij het vertrek. Annuleren is het enige exceptiepad dat doorsijpelt: gaat Options.CancellationToken af, dan wordt EPdfOperationCancelled opnieuw gegooid nadat het rapport is gevuld. TPdfXfaFlattenReport draagt ook SourceSha256 en OutputSha256, handig wanneer een batchtaak moet bewijzen welke invoer welk omgezet bestand opleverde. TPdfXfaFlattenOptions.Default stelt een packetbudget van 8 MiB in, 512 velden en een outputplafond van 512 MiB; de bron mag niet versleuteld zijn

De template- en datasets-packets lezen

PDFium Component accepteert beide lay-outs die ISO 32000-1 §12.7.8 voor de entry /XFA toestaat: de arrayvorm die packetnamen en streamreferenties afwisselt, en de single-stream XDP-vorm waarin één stream het hele document <xdp:xdp> vasthoudt. Packetstreams mogen ongecomprimeerd zijn of /FlateDecode zonder predictor, en decompressie is begrensd door MaxPacketBytes

Het single-stream geval is waar naïeve XML-afhandeling breekt. Designers schrijven doorgaans <xfa:datasets> met een namespace-prefix terwijl de template ongeprefixeerd staat, dus snijden op letterlijke tagnaam vindt één packet en mist het andere. De converter matcht element- en attribuutnamen op lokale naam, na een aparte controle dat openende en sluitende tags per hun volledige geprefixeerde naam paren vormen. Hoeft u alleen naar de packets te kijken in plaats van ze om te zetten, dan behandelt de gids voor het detecteren van XFA-formulieren en het extraheren van hun packets met PDFium VCL dat read-only pad

Veldwaarden komen van twee plekken. Een element <value> in de template levert de default, en het datasets-packet levert de ingevulde data, gebonden op veldnaam. Veldnamen herhalen zich in echte formulieren, dus de binder vergelijkt de keten van benoemde <subform>-voorouders met het voorouderpad in de data en neemt de langste niet-conflicterende match. Delen twee leafvelden een naam onder verschillende subforms, dan wordt de AcroForm-/T SubformPath.Name zodat de twee velden na de conversie onderscheiden blijven

Waarom heeft choiceList een expliciete Combo-vlag nodig?

XFA en PDF hebben tegenovergestelde defaults voor keuzevelden, dus een converter die geen flags kopieert maakt van elke drop-down een list box. Een XFA-<choiceList> met de default open="userControl" of met open="onEntry" is een drop-down; een PDF-veld /FT /Ch zonder flags is een scrollende list box, en alleen de Combo-vlag (bitpositie 18 in ISO 32000-1 Table 230, waarde 131072, dezelfde bit die PDFium FPDF_FORMFLAG_CHOICE_COMBO noemt) maakt er een combo box van. De converter zet daarom de Combo-vlag voor de drop-down-gevallen en laat de flags alleen leeg wanneer de template open="always" of open="multiSelect" zegt, wat de lijst permanent open houdt. Builds vóór 3.181.1 verwisselden de Combo- en Pushbutton-waarden en zochten naar een niet-standaard open="on", dus formulieren die ermee zijn afgevlakt zijn het opnieuw draaien waard

Keuzeveld-flagbeslissing in de PDFium Component conversie van statische XFA naar AcroForm: een open-attribuut van userControl of onEntry zet de Combo-vlag, bit 18 waarde 131072, om een drop-down combo box op te leveren, terwijl open always of multiSelect de flags leeg laat voor een permanent open list box
Zonder de Combo-vlag vlakt een XFA drop-down af tot een scrollende list box, dus de converter leest het open-attribuut en zet bit 18 alleen voor de drop-down-gevallen

De rest van de UI-mapping volgt de field flag-tabellen in ISO 32000-1 §12.7.4:

  • textEdit, numericEdit en dateTimeEdit worden /FT /Tx; een meerregelig tekstveld krijgt de Multiline-vlag (/Ff 4096, Table 228) en een over regels gesplitste appearance stream
  • passwordEdit wordt /FT /Tx met de Password-vlag (/Ff 8192) en een appearance die één asterisk per Unicode-scalar tekent, niet per UTF-16 code unit
  • checkButton wordt een check box /FT /Btn waarvan de waarde /Yes is wanneer de gebonden data 1 is en anders /Off
  • <exclGroup> wordt één radio-ouder /FT /Btn /Ff 32768 (Table 226) met /V, /DV, /Opt en /Kids, plus één kindwidget per lid met zijn eigen /AS en tweetoestands-/AP
  • imageEdit wordt een pushbutton waarvan de appearance de image schildert; een baseline JPEG wordt letterlijk ingebed onder /DCTDecode, terwijl PNG wordt gedecodeerd en opnieuw gecodeerd als /FlateDecode-pixels met een extra /SMask wanneer de PNG alpha of tRNS-transparantie draagt

Waarden die geen kale printbare ASCII zijn, inclusief regeleinden, worden geschreven als UTF-16BE-strings <FEFF...>, dus een Japanse naam of een adres over twee regels overleeft de reis. De Fields-output weerspiegelt wat er is geschreven, wat hem een handig audit-pad maakt

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, maateenheden en paginaroutering

XFA-geometrie wordt naar PDF user space omgezet door elk veld te spiegelen tegen de MediaBox-hoogte van zijn eigen pagina, want XFA meet omlaag vanaf de bovenkant van de pagina en PDF meet omhoog vanaf de onderkant. Verschuivingen van geneste <subform>-elementen stapelen zich op onderweg de template in, en elk veld moet met positieve breedte en hoogte eindigen of de conversie wordt geweigerd

Geometrieconversie van XFA naar PDF in PDFium Component voor Delphi: een veldrechthoek gemeten omlaag vanaf de bovenrand in de XFA-template wordt gespiegeld tegen de MediaBox-hoogte van zijn eigen pagina naar PDF user space, waar de y-coördinaat omhoog groeit vanaf de onderkant
De omlaag gerichte XFA-template en de omhoog gerichte PDF user space ontmoeten elkaar in één aftrekking tegen de MediaBox-hoogte, en dat is wat elke omgezette widget op zijn ontworpen positie zet

Metingen accepteren de eenheden die echte designers uitzenden: kale getallen en pt, plus in (72 punten), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) en mp (0.001, millipunten). Het parsen is locale-onafhankelijk, dus een Duitse Windows-installatie maakt van 8.5in geen onzin. Een onbekende eenheidsuffix wordt geweigerd in plaats van als punten gelezen te worden, wat het veld stilletjes zou verplaatsen

Paginaroutering volgt de <pageSet>: zusterelementen <pageArea> worden genummerd vanaf één in declaratievolgorde en claimen de onder hen geneste velden, terwijl velden zonder <pageArea>-voorouder op pagina één blijven. Elke pagina krijgt zijn eigen /Annots-update. Een veld dat voorbij de laatste pagina van de PDF wordt gerouteerd is een fout in plaats van een widget die op pagina één wordt gedropt

Welke XFA-formulieren weigert de converter?

De converter weigert alles waarvan de lay-out niet bekend kan zijn zonder de XFA-layoutengine te draaien. Dat betekent een attribuut layout anders dan position op een <subform> of <exclGroup> (flowed tb, lr-tb en soortgenoten, waar dynamische formulieren van zijn gebouwd), elk element <script>, en elke UI-soort buiten de zeven hierboven genoemde. Dynamische formulieren die tijdens run time pagina's laten groeien behoren tot een andere toolchain; het artikel over dynamische XFA-repaginering en pagina-aantallen in PDFium Component laat zien wat dat levende uitzetten inhoudt

De weigering is bewust. Een flowed subform heeft geen vaste coördinaten, dus elke "conversie" zou posities moeten verzinnen, en een gescript formulier berekent vaak totalen of zet zichtbaarheid om die een statische AcroForm niet kan reproduceren. Een PDF opleveren die compleet oogt maar zwijgend een berekend totaal verloor is erger dan een duidelijke fout in Report.ErrorText. De specificatie XFA 3.3 definieert veel meer dan deze subset, en de converter doet alsof niet

Valkuilen bij het schrijven en verifiëren van de AcroForm-revisie

Twee problemen doken op tijdens het bouwen van de writer, en allebei gelden voor iedereen die incrementele updates met de hand produceert. De eerste: de catalogus moet altijd tot de updateset behoren, ook wanneer zijn bytes niet veranderen omdat de AcroForm een elders herschreven indirect object is. De sparse update-writer leidt de trailer-/Root af uit de objecten die hij draagt, en een update zonder het rootobject levert een revisie op die viewers niet kunnen verankeren

De tweede betreft verificatie. Het geconverteerde bestand via de PDFium-wrapper laden triggert de initialisatie van de formulieromgeving zodra het document een /AcroForm heeft, en de host-pdfium.dll in de testopzet miste de export FORM_GetTextDirection, dus de initialisatie faalde voordat een enkel veld gecontroleerd kon worden. Terugleescontroles gebruiken daarom de componentobjectreader, die de actieve revisie parst zonder de formulierlaag aan te raken

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);
    // de actieve catalogus moet naar een AcroForm verwijzen; een inline AcroForm
    // geschreven door de converter draagt geen /XFA-entry
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Appearance streams zijn bewust simpel: Helvetica op 10 punten voor tekst- en keuzewaarden, een omlijnd vierkant voor check boxes, een gevuld binnenvierkant voor de geselecteerde radio-toestand. Ze maken het omgezette formulier overal leesbaar, maar ze zijn geen pixelkopie van de originele XFA-rendering, dus fonts, kleuren en randen die in de template zijn gedefinieerd worden niet meegenomen. Moet een formulier er identiek uitzien als de designer-output, render het dan een keer met de XFA-build en vergelijk voordat u een productiepipeline omschakelt

FAQ

Heeft FlattenPdfXfaToAcroForm de V8- of XFA-build van PDFium nodig?

Nee. FlattenPdfXfaToAcroForm parst de packets en schrijft de revisie met PDFium Component-code, dus hij werkt met de standaard pdfium.dll en voert geen enkel XFA-script uit

Kan de converter om met versleutelde XFA-documenten?

Niet rechtstreeks. Een versleutelde bron wordt met een fout geweigerd, dus ontsleutel eerst het document en zet de ontsleutelde kopie om

Zijn de omgezette velden nog bewerkbaar?

Ja. De output bevat echte AcroForm-tekst-, check box-, radio-, keuze- en pushbutton-velden met waarden en appearance streams, dus gebruikers kunnen het formulier blijven invullen in elke AcroForm-capabele viewer

Statische XFA-conversie, XFA-packetextractie en de XFA-runtime met V8 maken allemaal deel uit van de PDFium Component for Delphi and C++Builder, die PDFium omwikkelt voor Delphi-, C++Builder- en Lazarus-projecten