Teknisk artikel

Statiske XFA-formularer til AcroForm-felter i Delphi

PDFium Component konverterer statiske XFA-formularer til almindelige AcroForm-felter fra Delphi gennem FlattenPdfXfaToAcroForm i FPdfDocumentXfaFlatten-uniten. Funktionen læser XFA-template- og datasets-pakkerne, mapper hvert absolut positioneret felt til et AcroForm-terminalfelt med sin egen appearance stream og tilføjer resultatet som en trinvis revision, så den konverterede PDF åbner som en normal formular i viewers, der aldrig understøttede XFA

Motivationen er som regel en supportbillet frem for et roadmap-punkt. En offentlig eller forsikringsformular blev forfattet i et designværktøj, der gemte den som XFA, den virker i én desktop-reader, og alle andre steder åbner den som en blank side, der siger, at dokumentet behøver en nyere viewer. XFA blev depriceret i PDF 2.0 (ISO 32000-2), browsere og mobilviewers implementerede den aldrig, og PDFium selv renderer kun XFA med V8-aktiveret build. Statiske XFA-formularer, dem hvis layout er fastlagt på designtidspunktet, bærer nok geometri i templaten til at blive genopbygget som AcroForm-felter, og det er præcis det stykke, denne konverter håndterer

Hvad producerer FlattenPdfXfaToAcroForm egentlig?

FlattenPdfXfaToAcroForm producerer en byte-niveau AcroForm-revision tilføjet til en uændret kopi af kilden, aldrig en omskrivning af originalfilen. PDFium eksponerer ingen offentlig API til at skabe formularfelter, så komponenten går slet ikke gennem PDFium til dette job: den læser den aktive revision med sin egen objektreader, bygger felt-dictionaries, widget-annotationer, appearance XObjects og en /Helv-fontressource som rå PDF-objekter og serialiserer dem gennem en sparsom trinvis opdatering i ISO 32000-1 §7.5.6-forstand

Den nye /AcroForm-dictionary bærer /Fields, /DA og /DR og ingen /XFA-indgang. Beholder kilden sin AcroForm som et indirekte objekt, omskrives det objekt på stedet; en inline-dictionary erstattes inde i kataloget. Begge veje peger den aktive revision ikke længere på XFA-pakkerne, så en viewer, der forstår begge modeller, åbner AcroForm-felterne i stedet for at forsøge at laidte XFA-formularen ud. De oprindelige XFA-bytes er stadig i den tidligere revision, hvilket er det, en trinvis opdatering betyder, men intet i den aktuelle revision refererer dem

Statisk XFA-til-AcroForm-konverteringspipeline i PDFium Component til Delphi: FlattenPdfXfaToAcroForm læser template- og datasets-pakkerne, mapper absolut positionerede felter til felt-dictionaries og widgets og tilføjer AcroFormen som en sparsom trinvis revision uden en /XFA-indgang
FlattenPdfXfaToAcroForm rører aldrig de originale bytes: de konverterede felter, widgets og appearance streams tilføjes som en revision, hvis /AcroForm simpelthen holder op med at pege på XFA-pakkerne
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 er 512, det tilladte interval er 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;

Funktionen returnerer False og fylder Report.ErrorText for ethvert afvist eller misdannet input, og den gendanner kildestreamens position på vejen ud. Annullering er den eneste exception-sti, der propagerer: udløses Options.CancellationToken, rejses EPdfOperationCancelled igen, efter rapporten er fyldt. TPdfXfaFlattenReport bærer også SourceSha256 og OutputSha256, hvilket er praktisk, når et batchjob skal bevise, hvilket input der producerede hvilken konverteret fil. TPdfXfaFlattenOptions.Default sætter et pakkebudget på 8 MiB, 512 felter og et outputloft på 512 MiB; kilden må ikke være krypteret

Læsning af template- og datasets-pakkerne

PDFium Component accepterer begge layouts, som ISO 32000-1 §12.7.8 tillader for /XFA-indgangen: arrayformen, der skifter mellem pakkenavne og streamreferencer, og single-stream XDP-formen, hvor én stream holder hele <xdp:xdp>-dokumentet. Pakkestreams må være ukomprimerede eller /FlateDecode uden predictor, og dekomprimeringen afgrænses af MaxPacketBytes

Single-stream-tilfældet er dér, naiv XML-håndtering knækker. Designere skriver typisk <xfa:datasets> med et namespace-præfiks, mens templaten ligger upræfikseret, så skæring efter literal tagnavn finder én pakke og misser den anden. Konverteren matcher element- og attributnavne efter lokalt navn, efter et separat tjek af, at åbnings- og lukketags parres efter deres fulde præfikserede navn. Behøver du kun at kigge på pakkerne frem for at konvertere dem, dækker guiden til at detektere XFA-formularer og udpakke deres pakker med PDFium VCL den read-only-sti

Feltværdier kommer fra to steder. Et <value>-element i templaten leverer defaulten, og datasets-pakken leverer de udfyldte data, bundet efter feltnavn. Feltnavne gentager sig i rigtige formularer, så binderen sammenligner kæden af navngivne <subform>-forfædre med forfædrestien i dataene og tager det længste ikke-konfliktskabende match. Deler to bladfelter et navn under forskellige subforms, bliver AcroForm'ens /T til SubformPath.Name, så de to felter forbliver distinkte efter konverteringen

Hvorfor behøver choiceList et eksplicit Combo-flag?

XFA og PDF har modsatte defaults for choice-felter, så en konverter, der kopierer ingen flags, gør hver drop-down til en list box. En XFA <choiceList> med default open="userControl" eller med open="onEntry" er en drop-down; et PDF /FT /Ch-felt uden flags er en rullende list box, og kun Combo-flagget (bitposition 18 i ISO 32000-1 Tabel 230, værdi 131072, samme bit som PDFium navngiver FPDF_FORMFLAG_CHOICE_COMBO) gør den til en combo box. Konverteren sætter derfor Combo-flagget for drop-down-tilfældene og lader kun flags stå tomme, når templaten siger open="always" eller open="multiSelect", som holder listen permanent åben. Builds før 3.181.1 byttede Combo- og Pushbutton-værdierne og ledte efter en ikke-standard open="on", så formularer fladted med dem er værd at køre igen

Choice-felt-flag-beslutning i PDFium Components statiske XFA-til-AcroForm-konvertering: en open-attribut af userControl eller onEntry sætter Combo-flagget, bit 18 værdi 131072, til at producere en drop-down combo box, mens open always eller multiSelect lader flags stå tomme for en permanent åben list box
Uden Combo-flagget fladtes en XFA drop-down til en rullende list box, så konverteren læser open-attributten og sætter bit 18 kun for drop-down-tilfældene

Resten af UI-mappingen følger feltflag-tabellerne i ISO 32000-1 §12.7.4:

  • textEdit, numericEdit og dateTimeEdit bliver /FT /Tx; et flerlinjet tekstfelt får Multiline-flagget (/Ff 4096, Tabel 228) og en linjedelt appearance stream
  • passwordEdit bliver /FT /Tx med Password-flagget (/Ff 8192) og en appearance, der tegner ét asterisktegn pr. Unicode-scalar, ikke pr. UTF-16 kodeenhed
  • checkButton bliver et /FT /Btn check box, hvis værdi er /Yes, når de bundne data er 1, og /Off ellers
  • <exclGroup> bliver én /FT /Btn /Ff 32768 radio-forælder (Tabel 226) med /V, /DV, /Opt og /Kids plus ét child-widget pr. medlem med sin egen /AS og to-tilstands /AP
  • imageEdit bliver en pushbutton, hvis appearance maler billedet; en baseline JPEG indlejres ordret under /DCTDecode, mens PNG dekodes og re-encodes som /FlateDecode-pixels med en ekstra /SMask, når PNG'en bærer alpha eller tRNS-transparens

Værdier, der ikke er ren printbar ASCII, inklusive linjeskift, skrives som <FEFF...> UTF-16BE-strenge, så et japansk navn eller en tolinjet adresse overlever turen. Fields-outputtet spejler det, der blev skrevet, hvilket gør det til et bekvemt audit-spor

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;

Geometri, måleenheder og siderute

XFA-geometri konverteres til PDF user space ved at flippe hvert felt mod sin egen sides MediaBox-højde, fordi XFA måler nedad fra sidens top, og PDF måler opad fra bunden. Forskud fra indlejrede <subform>-elementer akkumulerer på vejen ned gennem templaten, og hvert felt skal ende med positiv bredde og højde, ellers afvises konverteringen

XFA-til-PDF-geometrikonvertering i PDFium Component til Delphi: et feltrektangel målt nedad fra toppen i XFA-templaten flips mod dens egen sides MediaBox-højde ind i PDF user space, hvor y-koordinaten vokser opad fra bunden
Den nedadvendte XFA-template og det opadvendte PDF user space mødes i én subtraktion mod MediaBox-højden, hvilket er det, der placerer hver konverteret widget på sin designede position

Mål accepterer de enheder, rigtige designere udsender: nøgne tal og pt plus in (72 points), mm (72 / 25,4), cm (72 / 2,54), pc (12), px (0,75) og mp (0,001, millipoint). Parsingen er locale-uafhængig, så en tysk Windows-installation forvandler ikke 8.5in til volapyk. Et ukendt enhedssuffiks afvises i stedet for at blive læst som points, hvilket stille ville fejlplacere feltet

Sideruten følger <pageSet>: sysken-<pageArea>-elementer nummereres fra én i erklæringsrækkefølge og gør krav på felterne indlejret under dem, mens felter uden en <pageArea>-forfader forbliver på side ét. Hver side får sin egen /Annots-opdatering. Et felt rutet forbi PDF'ens sidste side er en fejl frem for en widget droppet på side ét

Hvilke XFA-formularer afviser konverteren?

Konverteren afviser alt, hvis layout ikke kan kendes uden at køre XFA-layoutmotoren. Det betyder en layout-attribut andet end position på en <subform> eller et <exclGroup> (flowed tb, lr-tb og venner, hvilket er det, dynamiske formularer bygges af), ethvert <script>-element og enhver UI-kind uden for de syv listet ovenfor. Dynamiske formularer, der vokser sider ved kørselstid, tilhører en anden værktøjskæde; artiklen om dynamisk XFA-repaginering og sideantal i PDFium Component viser, hvad det live-layout indebærer

Afvisningen er bevidst. En flowed subform har ingen faste koordinater, så enhver "konvertering" måtte opfinde positioner, og en scriptet formular beregner ofte totaler eller toggler visibility, som en statisk AcroForm ikke kan gengive. At producere en PDF, der ser komplet ud men stille har mistet et beregnet total, er værre end en klar fejl i Report.ErrorText. XFA 3.3-specifikationen definerer langt mere end dette subsæt, og konverteren lader ikke, som om andet var tilfældet

Fælder ved skrivning og verificering af AcroForm-revisionen

To problemer viste sig under bygningen af writeren, og begge gælder enhver, der producerer trinvise opdateringer i hånden. Den første: kataloget skal altid være en del af opdateringssættet, selv når dets bytes ikke ændrer sig, fordi AcroFormen er et indirekte objekt omskrevet et andet sted. Den sparsomme opdateringswriter udleder traileren /Root fra de objekter, den bærer, og en opdatering uden rodobjektet producerer en revision, som viewers ikke kan forankre

Den anden vedrører verificering. Indlæsning af den konverterede fil gennem PDFium-wrapperen udløser form environment-initialisering, så snart dokumentet har en /AcroForm, og værts-pdfium.dll'en i testopsætningen manglede FORM_GetTextDirection-eksporten, så initialiseringen fejlede, før et eneste felt kunne tjekkes. Tilbage-læsningskontroller bruger derfor komponentens objektreader, som parser den aktive revision uden at røre formlaget

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);
    // det aktive katalog skal referere en AcroForm; en inline AcroForm
    // skrevet af konverteren bærer ingen /XFA-indgang
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Appearance streams er bevidst enkle: Helvetica ved 10 points til tekst- og choice-værdier, et omridset kvadrat til check boxes, et udfyldt indre kvadrat til den valgte radiotilstand. De gør den konverterede formular læsbar alle steder, men de er ikke en pixelkopi af den originale XFA-rendering, så fonts, farver og kanter defineret i templaten følger ikke med. Skal en formular se identisk ud med designer-outputtet, så render den én gang med XFA-builden og sammenlign, før du skifter en produktionspipeline over

FAQ

Behøver FlattenPdfXfaToAcroForm V8- eller XFA-builden af PDFium?

Nej. FlattenPdfXfaToAcroForm parser pakkerne og skriver revisionen med PDFium Component-kode, så den virker med den almindelige pdfium.dll og udfører intet XFA-script

Kan konverteren håndtere krypterede XFA-dokumenter?

Ikke direkte. En krypteret kilde afvises med en fejl, så dekryptér dokumentet først og konvertér den dekrypterede kopi

Er de konverterede felter stadig redigerbare?

Ja. Outputtet indeholder ægte AcroForm-tekst-, check box-, radio-, choice- og pushbutton-felter med værdier og appearance streams, så brugere kan blive ved med at udfylde formularen i enhver AcroForm-kapabel viewer

Statisk XFA-konvertering, XFA-pakkeudpakning og V8-aktiveret XFA-runtime er alle en del af PDFium Component for Delphi og C++Builder, som wrapper PDFium til Delphi-, C++Builder- og Lazarus-projekter