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
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
De rest van de UI-mapping volgt de field flag-tabellen in ISO 32000-1 §12.7.4:
textEdit,numericEditendateTimeEditworden/FT /Tx; een meerregelig tekstveld krijgt de Multiline-vlag (/Ff 4096, Table 228) en een over regels gesplitste appearance streampasswordEditwordt/FT /Txmet de Password-vlag (/Ff 8192) en een appearance die één asterisk per Unicode-scalar tekent, niet per UTF-16 code unitcheckButtonwordt een check box/FT /Btnwaarvan de waarde/Yesis wanneer de gebonden data1is en anders/Off<exclGroup>wordt één radio-ouder/FT /Btn /Ff 32768(Table 226) met/V,/DV,/Opten/Kids, plus één kindwidget per lid met zijn eigen/ASen tweetoestands-/APimageEditwordt 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/SMaskwanneer de PNG alpha oftRNS-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
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