Teknisk artikel

Konvertera statiska XFA-formulär till AcroForm-fält i Delphi

PDFium Component konverterar statiska XFA-formulär till vanliga AcroForm-fält från Delphi via FlattenPdfXfaToAcroForm i uniten FPdfDocumentXfaFlatten. Funktionen läser XFA-mallens och datasets-paketen, mappar varje absolut positionerat fält till ett AcroForm-terminalfält med sin egen utseendeström, och fogar resultatet som en inkrementell revision, så att den konverterade PDF:en öppnas som ett vanligt formulär i visare som aldrig stödde XFA

Motivationen är oftast ett supportärende snarare än en roadmap-post. Ett statligt eller försäkringsformulär författades i en designer som sparade det som XFA, det fungerar i en skrivbordsläsare, och överallt annars öppnas det som en blank sida som säger att dokumentet kräver en nyare visare. XFA fasades ut i PDF 2.0 (ISO 32000-2), webbläsare och mobila visare implementerade det aldrig, och PDFium i sig renderar bara XFA med V8-aktiverat bygge. Statiska XFA-formulär, de vars layout är låst vid designtid, bär tillräckligt med geometri i mallen för att byggas om som AcroForm-fält, och det är exakt den skiva den här konvertern hanterar

Vad producerar FlattenPdfXfaToAcroForm egentligen?

FlattenPdfXfaToAcroForm producerar en byte-nivå-AcroForm-revision fogad till en oförändrad kopia av källan, aldrig en omskrivning av originalfilen. PDFium exponerar inget publikt API för att skapa formulärfält, så komponenten går inte alls via PDFium för det här jobbet: den läser den aktiva revisionen med sin egen objektläsare, bygger fältordböcker, widgetannoteringar, utseende-XObjects och en /Helv-teckensnittsresurs som råa PDF-objekt, och serialiserar dem genom en gles inkrementell uppdatering i meningen ISO 32000-1 §7.5.6

Den nya /AcroForm-ordboken bär /Fields, /DA och /DR och ingen /XFA-post. När källan behåller sin AcroForm som ett indirekt objekt skrivs det objektet om på plats; en inline-ordbok ersätts inne i katalogen. Oavsett vilket pekar den aktiva revisionen inte längre på XFA-paketen, så en visare som förstår båda modellerna öppnar AcroForm-fälten i stället för att försöka lägga ut XFA-formuläret. Originalens XFA-byte ligger kvar i den tidigare revisionen, vilket är vad en inkrementell uppdatering betyder, men ingenting i den aktuella revisionen refererar dem

Pipeline för statisk XFA till AcroForm-konvertering i PDFium Component för Delphi: FlattenPdfXfaToAcroForm läser mall- och datasets-paketen, mappar absolut positionerade fält till fältordböcker och widgetar, och fogar AcroForm:en som en gles inkrementell revision utan /XFA-post
FlattenPdfXfaToAcroForm rör aldrig originalbytena: de konverterade fälten, widgetarna och utseendeströmmarna fogas som en revision vars /AcroForm helt enkelt slutar peka på XFA-paketen
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;   // standard är 512, tillåtet intervall är 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 returnerar False och fyller Report.ErrorText för avvisad eller felformad indata, och den återställer källströmmens position på vägen ut. Avbrott är den enda undantagsväg som fortplantar sig: utlöses Options.CancellationToken kastas EPdfOperationCancelled om igen efter att rapporten fyllts. TPdfXfaFlattenReport bär också SourceSha256 och OutputSha256, vilket är praktiskt när ett batchjobb måste bevisa vilken indata som producerade vilken konverterad fil. TPdfXfaFlattenOptions.Default sätter en paketbudget på 8 MiB, 512 fält och ett utdatatak på 512 MiB; källan får inte vara krypterad

Att läsa mallens och datasetsens paket

PDFium Component accepterar båda layouter ISO 32000-1 §12.7.8 tillåter för /XFA-posten: arrayformen som växlar paketnamn och strömreferenser, och XDP-formen med en enda ström där en ström håller hela <xdp:xdp>-dokumentet. Paketströmmar får vara okomprimerade eller /FlateDecode utan prediktor, och dekomprimeringen avgränsas av MaxPacketBytes

Enströmsfallet är där naiv XML-hantering bryter ihop. Designers skriver vanligen <xfa:datasets> med ett namnrymdsprefix medan mallen ligger oprefixad, så att skära efter bokstavligt taggnamn hittar ett paket och missar det andra. Konvertern matchar element- och attributnamn efter lokalt namn, efter en separat kontroll av att öppnande och stängande taggar paras ihop efter sitt fullständigt prefixade namn. Behöver du bara titta på paketen snarare än konvertera dem täcker guiden till att upptäcka XFA-formulär och extrahera deras paket med PDFium VCL den läsbara vägen

Fältvärden kommer från två ställen. Ett <value>-element i mallen levererar standardvärdet, och datasets-paketet levererar det ifyllda, bundet efter fältnamn. Fältnamn upprepas i verkliga formulär, så bindaren jämför kedjan av namngivna <subform>-förfäder med förfädersvägen i datan och tar den längsta icke-konflikterande matchen. När två lövfält delar ett namn under olika subformulär blir AcroForm:ens /T SubformPath.Name så att de två fälten förblir åtskilda efter konverteringen

Varför behöver choiceList en explicit Combo-flagga?

XFA och PDF har motsatta standardvärden för valfält, så en konverter som kopierar inga flaggor gör om varje rullgardin till en listruta. Ett XFA <choiceList> med standard open="userControl" eller med open="onEntry" är en rullgardin; ett PDF /FT /Ch-fält utan flaggor är en rullande listruta, och bara Combo-flaggan (bitposition 18 i ISO 32000-1 tabell 230, värde 131072, samma bit som PDFium namnger FPDF_FORMFLAG_CHOICE_COMBO) gör den till en kombinationsruta. Konvertern sätter därför Combo-flaggan för rullgardinsfallen och lämnar flaggorna tomma bara när mallen säger open="always" eller open="multiSelect", som håller listan permanent öppen. Byggen före 3.181.1 bytte plats på Combo- och Pushbutton-värdena och letade efter en icke-standard open="on", så formulär plattade med dem är värda att köra igen

Beslut av valfältsflaggor i PDFium Components statiska XFA-till-AcroForm-konvertering: ett open-attribut av userControl eller onEntry sätter Combo-flaggan, bit 18 värde 131072, för att ge en rullgardinskombinationsruta, medan open always eller multiSelect lämnar flaggorna tomma för en permanent öppen listruta
Utan Combo-flaggan plattas en XFA-rullgardin till en rullande listruta, så konvertern läser open-attributet och sätter bit 18 bara för rullgardinsfallen

Resten av UI-mappningen följer fältflaggtabellerna i ISO 32000-1 §12.7.4:

  • textEdit, numericEdit och dateTimeEdit blir /FT /Tx; ett textfält med flera rader får Multiline-flaggan (/Ff 4096, tabell 228) och en radbruten utseendeström
  • passwordEdit blir /FT /Tx med Password-flaggan (/Ff 8192) och ett utseende som ritar en asterisk per Unicode-skalär, inte per UTF-16-kodenhet
  • checkButton blir en /FT /Btn-kryssruta vars värde är /Yes när den bundna datan är 1 och /Off annars
  • <exclGroup> blir en /FT /Btn /Ff 32768-radioförälder (tabell 226) med /V, /DV, /Opt och /Kids, plus en barnwidget per medlem med egen /AS och tvåtillstånds-/AP
  • imageEdit blir en tryckknapp vars utseende målar bilden; en baseline-JPEG bäddas in ordagrant under /DCTDecode, medan PNG avkodas och kodas om som /FlateDecode-pixlar med en extra /SMask när PNG:en bär alfa- eller tRNS-transparens

Värden som inte är vanlig utskrivbar ASCII, inklusive radbrytningar, skrivs som <FEFF...> UTF-16BE-strängar, så ett japanskt namn eller en tvåradig adress överlever resan. Fields-utdata speglar vad som skrevs, vilket gör den till ett bekvämt spårningsspår

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åttenheter och sidroutning

XFA-geometri konverteras till PDF-användarutrymme genom att vända varje fält mot sin egen sidas MediaBox-höjd, eftersom XFA mäter nedåt från sidans topp och PDF mäter uppåt från botten. Offset från nästlade <subform>-element ackumuleras på vägen ner genom mallen, och varje fält måste sluta med positiv bredd och höjd eller avvisas konverteringen

Geometrikonvertering från XFA till PDF i PDFium Component för Delphi: en fältrektangel mätt nedåt från överkanten i XFA-mallen vänds mot dess egen sidas MediaBox-höjd in i PDF-användarutrymme, där y-koordinaten växer uppåt från botten
Den nedåtgående XFA-mallen och det uppåtgående PDF-användarutrymmet möts i en enda subtraktion mot MediaBox-höjden, vilket är det som placerar varje konverterad widget på sin designade position

Mått accepterar de enheter verkliga designers sänder ut: blotta tal och pt, plus in (72 punkter), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) och mp (0.001, millipunkter). Parsningen är lokaloberoende, så en tysk Windows-installation gör inte 8.5in till nonsens. Ett okänt enhetssuffix avvisas i stället för att läsas som punkter, vilket tyst skulle missplacera fältet

Sidroutning följer <pageSet>: syskon-<pageArea>-element numreras från ett i deklarationsordning och gör anspråk på fälten nästlade under dem, medan fält utan <pageArea>-förfader stannar på sida ett. Varje sida får sin egen /Annots-uppdatering. Ett fält routat förbi PDF:ens sista sida är ett fel i stället för en widget tappad på sida ett

Vilka XFA-formulär vägrar konvertern?

Konvertern vägrar allt vars layout inte kan känna till utan att köra XFA-layoutmotorn. Det betyder ett layout-attribut annat än position på ett <subform> eller <exclGroup> (flödade tb, lr-tb och vänner, vilket är vad dynamiska formulär byggs av), varje <script>-element, och varje UI-slag utöver de sju listade ovan. Dynamiska formulär som växer sidor vid körtid tillhör en annan verktygskedja; artikeln om dynamisk XFA-ompaginering och sidantal i PDFium Component visar vad den levande layouten innebär

Vägran är medveten. Ett flödat subformulär har inga fasta koordinater, så varje "konvertering" skulle behöva hitta på positioner, och ett skriptat formulär beräknar ofta summor eller växlar synlighet som ett statiskt AcroForm inte kan återskapa. Att producera en PDF som ser komplett ut men tyst tappade en beräknad summa är värre än ett tydligt fel i Report.ErrorText. XFA 3.3-specifikationen definierar betydligt mer än denna delmängd, och konvertern låtsas inte annars

Fallgropar vid skrivande och verifiering av AcroForm-revisionen

Två problem dök upp medan skrivaren byggdes, och båda gäller vem som helst som producerar inkrementella uppdateringar för hand. Den första: katalogen måste alltid vara del av uppdateringsmängden, även när dess byte inte ändras eftersom AcroForm:en är ett indirekt objekt omskrivet annanstans. Den glesa uppdateringsskrivaren härleder trailerns /Root från de objekt den bär, och en uppdatering utan rotobjektet ger en revision som visare inte kan förankra

Den andra gäller verifieringen. Att läsa in den konverterade filen genom PDFium-wrappern utlöser formulärmiljöinitialisering så snart dokumentet har en /AcroForm, och värd-pdfium.dllen i testuppsättningen saknade exporten FORM_GetTextDirection, så initialiseringen misslyckades innan ett enda fält kunde kontrolleras. Tillbakaläsningskontroller använder därför komponentens objektläsare, som parsar den aktiva revisionen utan att röra formulärlagret

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);
    // den aktiva katalogen måste referera till en AcroForm; en inline-AcroForm
    // skriven av konvertern bär ingen /XFA-post
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Utseendeströmmar är medvetet enkla: Helvetica i 10 punkter för text- och valvärden, en streckad kvadrat för kryssrutor, en fylld innre kvadrat för det valda radioläget. De gör det konverterade formuläret läsbart överallt, men de är ingen pixilkopia av den ursprungliga XFA-renderingen, så typsnitt, färger och ramar definierade i mallen förs inte över. Måste ett formulär se identiskt ut med designer-resultatet, rendera det en gång med XFA-bygget och jämför innan du växlar en produktionspipeline över

FAQ

Behöver FlattenPdfXfaToAcroForm V8- eller XFA-bygget av PDFium?

Nej. FlattenPdfXfaToAcroForm parsar paketen och skriver revisionen med PDFium Component-kod, så den fungerar med vanliga pdfium.dll och exekverar inget XFA-skript

Kan konvertern hantera krypterade XFA-dokument?

Inte direkt. En krypterad källa avvisas med ett fel, så dekryptera dokumentet först och konvertera den dekrypterade kopian

Är de konverterade fälten fortfarande redigerbara?

Ja. Utdata innehåller riktiga AcroForm-text-, kryssrute-, radio-, val- och tryckknappsfält med värden och utseendeströmmar, så användare kan fortsätta fylla i formuläret i vilken AcroForm-kapabel visare som helst

Statisk XFA-konvertering, XFA-paketextraktion och den V8-aktiverade XFA-körtiden är alla del av PDFium Component for Delphi and C++Builder, som wrappar PDFium för Delphi-, C++Builder- och Lazarus-projekt