Article technique

Convertir du XFA statique en champs AcroForm en Delphi

PDFium Component convertit les formulaires XFA statiques en champs AcroForm ordinaires depuis Delphi via FlattenPdfXfaToAcroForm dans lunité FPdfDocumentXfaFlatten. La fonction lit les paquets template et datasets XFA, mappe chaque champ absolument positionné vers un champ terminal AcroForm avec son propre flux dapparence, et ajoute le résultat comme une révision incrémentale, si bien que le PDF converti souvre comme un formulaire normal dans des visionneuses qui nont jamais pris en charge XFA

La motivation est dordinaire un ticket de support plutôt quune entrée de feuille de route. Un formulaire gouvernemental ou dassurance a été rédigé dans un outil de conception qui la sauvegardé en XFA, il marche dans un lecteur de bureau, et partout ailleurs il souvre en page vierge qui dit que le document exige une visionneuse plus récente. XFA a été déprécié dans PDF 2.0 (ISO 32000-2), les navigateurs et visionneuses mobiles ne la jamais implémenté, et PDFium lui-même ne rend le XFA quavec la build à V8 activée. Les formulaires XFA statiques, ceux dont la mise en page est fixée à la conception, portent assez de géométrie dans le template pour être reconstruits en champs AcroForm, et cest exactement la tranche que ce convertisseur gère

Que produit réellement FlattenPdfXfaToAcroForm ?

FlattenPdfXfaToAcroForm produit une révision AcroForm au niveau octets ajoutée à une copie inchangée de la source, jamais une réécriture du fichier dorigine. PDFium nexpose aucune API publique pour créer des champs de formulaire, si bien que le composant ne passe pas du tout par PDFium pour ce travail : il lit la révision active avec son propre lecteur dobjets, construit dictionnaires de champs, annotations widget, XObjects dapparence et une ressource de police /Helv comme objets PDF bruts, et les sérialise au travers dune mise à jour incrémentale creuse au sens dISO 32000-1 §7.5.6

Le nouveau dictionnaire /AcroForm porte /Fields, /DA et /DR et aucune entrée /XFA. Quand la source garde son AcroForm comme objet indirect, cet objet est réécrit en place ; un dictionnaire en ligne est remplacé à lintérieur du catalogue. Dans les deux cas, la révision active ne pointe plus les paquets XFA, si bien quune visionneuse qui comprend les deux modèles ouvre les champs AcroForm au lieu de tenter la mise en page du formulaire XFA. Les octets XFA dorigine sont toujours dans la révision antérieure, ce quest une mise à jour incrémentale, mais rien dans la révision courante ne les référence

Pipeline de conversion XFA statique vers AcroForm dans PDFium Component pour Delphi : FlattenPdfXfaToAcroForm lit les paquets template et datasets, mappe les champs absolument positionnés vers dictionnaires de champs et widgets, et ajoute lAcroForm comme une révision incrémentale creuse sans entrée /XFA
FlattenPdfXfaToAcroForm ne touche jamais les octets dorigine : les champs convertis, widgets et flux dapparence sont ajoutés comme une révision dont lAcroForm cesse simplement de pointer les paquets 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;   // le défaut est 512, la plage autorisée est 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;

La fonction renvoie False et remplit Report.ErrorText pour toute entrée rejetée ou mal formée, et elle restaure la position du flux source en sortant. Lannulation est le seul chemin dexception qui se propage : si Options.CancellationToken tire, EPdfOperationCancelled est relancée après que le rapport est rempli. TPdfXfaFlattenReport porte aussi SourceSha256 et OutputSha256, pratique quand un travail par lots doit prouver quelle entrée a produit quel fichier converti. TPdfXfaFlattenOptions.Default règle un budget de paquets de 8 Mio, 512 champs et un plafond de sortie de 512 Mio ; la source ne doit pas être chiffrée

Lire les paquets template et datasets

PDFium Component accepte les deux dispositions quISO 32000-1 §12.7.8 autorise pour lentrée /XFA : la forme tableau qui alterne noms de paquets et références de flux, et la forme XDP à flux unique où un seul flux détient tout le document <xdp:xdp>. Les flux de paquets peuvent être non compressés ou /FlateDecode sans prédicteur, et la décompression est bornée par MaxPacketBytes

Le cas à flux unique est là où une gestion XML naïve casse. Les outils de conception écrivent couramment <xfa:datasets> avec un préfixe despace de noms tandis que le template siège sans préfixe, si bien que découper par nom de balise littéral trouve un paquet et rate lautre. Le convertisseur apparie les noms déléments et dattributs par nom local, après un contrôle séparé que les balises ouvrantes et fermantes sapparient par leur nom complet préfixé. Si vous navez besoin que de regarder les paquets plutôt que de les convertir, le guide de détection des formulaires XFA et dextraction de leurs paquets avec PDFium VCL couvre ce chemin en lecture seule

Les valeurs de champs viennent de deux endroits. Un élément <value> dans le template fournit le défaut, et le paquet datasets fournit les données remplies, liées par nom de champ. Les noms de champs se répètent dans les vrais formulaires, si bien que le lieur compare la chaîne dancêtres <subform> nommés avec le chemin dancêtres dans les données et prend la plus longue correspondance non conflictuelle. Quand deux champs feuilles partagent un nom sous des sous-formulaires différents, le /T AcroForm devient SubformPath.Name pour que les deux champs restent distincts après conversion

Pourquoi choiceList a-t-il besoin dun drapeau Combo explicite ?

XFA et PDF ont des défauts opposés pour les champs de choix, si bien quun convertisseur qui ne copie aucun drapeau transforme chaque liste déroulante en list box. Un <choiceList> XFA avec le défaut open="userControl" ou avec open="onEntry" est une liste déroulante ; un champ PDF /FT /Ch sans drapeaux est une list box défilante, et seul le drapeau Combo (position de bit 18 dans ISO 32000-1 Table 230, valeur 131072, le même bit que PDFium nomme FPDF_FORMFLAG_CHOICE_COMBO) en fait une combo box. Le convertisseur pose donc le drapeau Combo pour les cas de listes déroulantes et laisse les drapeaux vides seulement quand le template dit open="always" ou open="multiSelect", qui gardent la liste ouverte en permanence. Les builds antérieures à 3.181.1 échangeaient les valeurs Combo et Pushbutton et cherchaient un open="on" non standard, si bien que les formulaires aplatis avec elles valent dêtre repassés

Décision de drapeaux de champ de choix dans la conversion XFA statique vers AcroForm de PDFium Component : un attribut open de userControl ou onEntry pose le drapeau Combo, bit 18 valeur 131072, pour produire une combo box déroulante, tandis que open always ou multiSelect laisse les drapeaux vides pour une list box ouverte en permanence
Sans le drapeau Combo une liste déroulante XFA saplatit en list box défilante, si bien que le convertisseur lit lattribut open et pose le bit 18 seulement pour les cas de listes déroulantes

Le reste du mappage dinterface suit les tables de drapeaux de champs dISO 32000-1 §12.7.4 :

  • textEdit, numericEdit et dateTimeEdit deviennent /FT /Tx ; un champ texte multi-ligne reçoit le drapeau Multiline (/Ff 4096, Table 228) et un flux dapparence à lignes scindées
  • passwordEdit devient /FT /Tx avec le drapeau Password (/Ff 8192) et une apparence qui dessine un astérisque par scalaire Unicode, pas par unité de code UTF-16
  • checkButton devient une case à cocher /FT /Btn dont la valeur est /Yes quand les données liées valent 1 et /Off sinon
  • <exclGroup> devient un parent radio /FT /Btn /Ff 32768 (Table 226) avec /V, /DV, /Opt et /Kids, plus un widget enfant par membre avec son propre /AS et un /AP à deux états
  • imageEdit devient un pushbutton dont lapparence peint limage ; un JPEG baseline est incorporé verbatim sous /DCTDecode, tandis que le PNG est décodé et réencodé en pixels /FlateDecode avec un /SMask supplémentaire quand le PNG porte une transparence alpha ou tRNS

Les valeurs qui ne sont pas de lASCII imprimable simple, sauts de ligne compris, sont écrites comme des chaînes <FEFF...> UTF-16BE, si bien quun nom japonais ou une adresse sur deux lignes survit au voyage. La sortie Fields reflète ce qui a été écrit, ce qui en fait une piste daudit commode

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;

Géométrie, unités de mesure et routage de pages

La géométrie XFA est convertie en espace utilisateur PDF en retournant chaque champ contre la hauteur MediaBox de sa propre page, parce que XFA mesure vers le bas depuis le haut de la page et le PDF mesure vers le haut depuis le bas. Les décalages des éléments <subform> imbriqués saccumulent en descendant le template, et chaque champ doit finir avec largeur et hauteur positives ou la conversion est rejetée

Conversion de géométrie XFA vers PDF dans PDFium Component pour Delphi : un rectangle de champ mesuré vers le bas depuis le bord supérieur dans le template XFA est retourné contre la hauteur MediaBox de sa propre page vers lespace utilisateur PDF, où la coordonnée y croît vers le haut depuis le bas
Le template XFA vers le bas et lespace utilisateur PDF vers le haut se rejoignent en une soustraction contre la hauteur MediaBox, ce qui place chaque widget converti à sa position conçue

Les mesures acceptent les unités que les vrais outils de conception émettent : nombres nus et pt, plus in (72 points), mm (72 / 25.4), cm (72 / 2.54), pc (12), px (0.75) et mp (0.001, millipoints). Lanalyse est indépendante de la locale, si bien quune installation Windows allemande ne transforme pas 8.5in en absurdité. Un suffixe dunité inconnu est rejeté au lieu dêtre lu comme des points, ce qui déplacerait silencieusement le champ

Le routage de pages suit le <pageSet> : les éléments <pageArea> frères sont numérotés à partir de un dans lordre de déclaration et réclament les champs imbriqués sous eux, tandis que les champs sans ancêtre <pageArea> restent en page un. Chaque page reçoit sa propre mise à jour /Annots. Un champ routé au-delà de la dernière page du PDF est une erreur plutôt quun widget abandonné en page un

Quels formulaires XFA le convertisseur refuse-t-il ?

Le convertisseur refuse tout ce dont la mise en page ne peut pas être connue sans faire tourner le moteur de mise en page XFA. Cela veut dire un attribut layout autre que position sur un <subform> ou un <exclGroup> (les flux tb, lr-tb et compagnie, qui constituent les formulaires dynamiques), tout élément <script>, et toute espèce dinterface hors des sept listées ci-dessus. Les formulaires dynamiques qui gagnent des pages à lexécution relèvent dune autre chaîne doutils ; larticle sur la repagination XFA dynamique et les comptes de pages dans PDFium Component montre ce que cette mise en page vivante implique

Le refus est délibéré. Un sous-formulaire en flux na pas de coordonnées fixes, si bien que toute "conversion" devrait inventer des positions, et un formulaire scripté calcule souvent des totaux ou bascule des visibilités quun AcroForm statique ne peut pas reproduire. Produire un PDF qui a lair complet mais qui a perdu en silence un total calculé est pire quune erreur claire dans Report.ErrorText. La spécification XFA 3.3 définit bien plus que ce sous-ensemble, et le convertisseur ne prétend pas le contraire

Pièges en écrivant et vérifiant la révision AcroForm

Deux problèmes sont apparus pendant la construction du rédacteur, et tous deux sappliquent à quiconque produit des mises à jour incrémentales à la main. Le premier : le catalogue doit toujours faire partie de lensemble de mise à jour, même quand ses octets ne changent pas parce que lAcroForm est un objet indirect réécrit ailleurs. Le rédacteur de mise à jour creuse dérive le /Root du trailer depuis les objets quil porte, et une mise à jour sans lobjet racine produit une révision que les visionneuses ne peuvent pas ancrer

Le second concerne la vérification. Charger le fichier converti au travers de lenveloppe PDFium déclenche linitialisation de lenvironnement de formulaires dès que le document a un /AcroForm, et le pdfium.dll hôte du banc de test manquait de lexport FORM_GetTextDirection, si bien que linitialisation échouait avant quun seul champ puisse être contrôlé. Les contrôles de relecture emploient donc le lecteur dobjets du composant, qui analyse la révision active sans toucher la couche de formulaires

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);
    // le catalogue actif doit référencer un AcroForm ; un AcroForm en ligne
    // écrit par le convertisseur ne porte aucune entrée /XFA
    Result := Pos('/AcroForm', string(Revision.CatalogDictionary)) > 0;
  finally
    Stream.Free;
  end;
end;

Les flux dapparence sont délibérément simples : Helvetica à 10 points pour les valeurs de texte et de choix, un carré au trait pour les cases à cocher, un carré intérieur plein pour létat radio sélectionné. Ils rendent le formulaire converti lisible partout, mais ils ne sont pas une copie pixel du rendu XFA dorigine, si bien que polices, couleurs et bordures définies dans le template ne sont pas reportées. Si un formulaire doit ressembler identiquement à la sortie de loutil de conception, rendez-le une fois avec la build XFA et comparez avant de basculer un pipeline de production

FAQ

FlattenPdfXfaToAcroForm exige-t-il la build V8 ou XFA de PDFium ?

Non. FlattenPdfXfaToAcroForm analyse les paquets et écrit la révision avec du code PDFium Component, si bien quil marche avec le pdfium.dll standard et nexécute aucun script XFA

Le convertisseur peut-il gérer des documents XFA chiffrés ?

Pas directement. Une source chiffrée est rejetée avec une erreur, donc déchiffrez dabord le document et convertissez la copie déchiffrée

Les champs convertis restent-ils éditables ?

Oui. La sortie contient de vrais champs AcroForm texte, case à cocher, radio, choix et pushbutton avec valeurs et flux dapparence, si bien que les utilisateurs peuvent continuer à remplir le formulaire dans toute visionneuse capable dAcroForm

La conversion XFA statique, lextraction de paquets XFA et le moteur XFA à V8 activée font tous partie du PDFium Component for Delphi and C++Builder, qui enveloppe PDFium pour des projets Delphi, C++Builder et Lazarus