Un formulaire PDF avec des valeurs de champs mais sans flux d'apparence peut sortir d'un travail de rendu Linux en boîtes vides, en mauvaise police ou mauvais alignement, ou avec une EHPDFHeadlessRenderError lisant "Widget field requires a name field type". Le moteur de rendu headless de HotPDF dessine des apparences temporaires pour les widgets de texte, de bouton et de choix, en héritant /FT, /V, /DA et /Q à travers l'arbre des champs
Où un widget garde ses réglages de champ
La plupart des ennuis commencent avec la séparation entre un champ et ses widgets. Le dictionnaire de champ détient la partie logique d'un contrôle de formulaire : son type, sa valeur, ses drapeaux et son apparence par défaut. L'annotation widget détient ce dont une page a besoin : /Rect, bordure, un pointeur vers sa page. Un champ avec un seul widget est d'ordinaire fusionné en un dictionnaire unique. Quand la même valeur doit apparaître sur plusieurs pages, les widgets deviennent des enfants du champ et ne portent souvent rien d'autre que la géométrie et une référence /Parent. L'ISO 32000-1 §12.7.3.1 rend /FT, /Ff, /V et /DV héritables le long de cette chaîne (tableau 220), et le §12.7.3.3 n'ajoute que deux attributs de texte variable à l'ensemble héritable du tableau 222 : /DA, qui est requis mais peut venir d'un ancêtre, et /Q. Le dictionnaire de formulaire interactif (tableau 218) fournit les valeurs par défaut à l'échelle du document pour /DA et /Q, plus /DR, le dictionnaire de ressources par défaut dans lequel les noms de polices de /DA sont recherchés. /DR n'est pas dans le tableau 222 ; il appartient au formulaire dans son ensemble. Un moteur de rendu qui ne lit que le dictionnaire widget ne trouve ni type, ni valeur, ni /DA dans le fichier ci-dessous et retombe sur sa police par défaut
1 0 obj % catalogue
<< /Type /Catalog /Pages 2 0 R
/AcroForm << /Fields [10 0 R] /DA (/Helv 0 Tf 0 g) /Q 0
/DR << /Font << /Helv 20 0 R /Cour 21 0 R >> >> >> >>
endobj
10 0 obj % champ terminal : type, valeur et apparence par défaut résident ici
<< /FT /Tx /T (invoice_no) /V (INV-2026-0417)
/DA (/Cour 10 Tf 0 0 0.6 rg) /Q 2
/Kids [11 0 R 12 0 R] >>
endobj
11 0 obj % widget de la page 1 : géométrie seule, coin haut droite donné en premier
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % widget de la page 2 : géométrie seule
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF cherche chaque entrée de champ sur le widget et, à défaut, grimpe le /Parent un dictionnaire à la fois ; la valeur la plus proche gagne, et seuls /DA et /Q retombent sur le dictionnaire AcroForm une fois la chaîne épuisée. Attendu pour le fichier ci-dessus : les deux widgets affichent INV-2026-0417 alignés à droite en Courier 10 points, bleu foncé, et le /Helv 0 Tf à l'échelle du document n'entre jamais en jeu parce que l'objet 10 a son propre /DA. Quand un champ sort en Helvetica noire 12 points, le suspect évident est une police Courier absente de la machine Linux. La taille la trahit. C'est ce que HotPDF dessine quand aucun /DA ne se présente sur le widget, sur un ancêtre ou dans le dictionnaire AcroForm. Alors videz le widget, suivez son /Parent à la main et trouvez où le producteur a posé le /DA, par exemple sur un dictionnaire de champ vers lequel le widget ne renvoie jamais
Les noms de polices de /DA se résolvent contre un /DR posé sur le widget ou un champ parent, le /DR de l'AcroForm et les ressources de polices de la page, dans cet ordre. Un /DR au niveau du champ est hors du tableau 222, mais il est sans ambiguïté quand il est présent, si bien que HotPDF l'accepte. La grimpe est bornée : une boucle dans l'arbre s'arrête avec "Widget field inheritance contains a cycle", un /Parent qui n'est pas un dictionnaire avec "Widget Parent requires a dictionary", et une chaîne plus profonde que MaxDepth dans les options de rendu (32 par défaut) avec "Widget field inheritance depth budget exceeded". Le composant VCL applique le même héritage quand il lit les valeurs AcroForm héritées et réinitialise les champs
Comment le moteur headless décide-t-il quoi dessiner ?
HPDFRenderHeadlessPagePNG et sa variante fichier ne lancent le repli que pour un widget visible dont le /AP est absent ou null ; l'ISO 32000-1 §7.3.7 dit qu'une valeur de dictionnaire null vaut une entrée absente, et le moteur traite /AP null ainsi. Une apparence normale présente gagne toujours, quoi que dise /NeedAppearances, si bien qu'une apparence périmée issue d'un remplissage antérieur continue de s'afficher jusqu'à ce que quelque chose la régénère. Le document chargé n'est jamais modifié. Ce qui se dessine dépend du type de champ résolu et des drapeaux :
| Champ | Apparence synthétisée |
|---|---|
Texte une ligne, /FT /Tx | La chaîne /V, composée avec le /DA et le /Q résolus à l'intérieur du /Rect |
Case à cocher, /FT /Btn | Bordure rectangulaire avec rayons d'angle issus de /Border, remplissage /MK /BG, et une coche vectorielle tracée quand cochée |
| Bouton radio, Btn avec bit 16 | Bordure circulaire avec un point plein quand actif |
| Bouton poussoir, Btn avec bit 17 | Bordure rectangulaire avec la légende /MK /CA centrée |
Liste déroulante, /FT /Ch avec bit 18 | Le texte d'affichage de l'entrée /Opt sélectionnée comme légende centrée |
| Liste, Ch avec bit 18 à zéro | Une rangée par option à partir de /TI, avec une bande de surbrillance derrière chaque rangée sélectionnée |
La coche et le point radio sont des tracés vectoriels plutôt que des glyphes ZapfDingbats, la manière dont beaucoup de générateurs les dessinent, parce qu'un conteneur Linux mince peut n'avoir ZapfDingbats d'installé. Les faces de bouton poussoir et de liste déroulante correspondent à ce que le générateur d'apparences VCL de HotPDF dessine pour le même champ, si bien qu'un formulaire se ressemble quel que soit le côté qui a produit l'image. Une légende prend sa police, sa taille et sa couleur de /DA ; puisque la mise en page des légendes ne s'adapte pas automatiquement, une taille /DA de 0 devient 70 pour cent de la hauteur de la boîte, plafonné à 12 points. L'état des cases à cocher et des boutons radio vient du /AS propre au widget, à défaut le /V hérité, et tout état autre que Off dessine la marque. Donnez à chaque enfant radio son propre /AS. Le /V du parent nomme l'état sélectionné pour le groupe entier et ne sait distinguer un enfant d'un autre
uses
SysUtils, HPDFHeadlessDocument, HPDFHeadlessCairoDevice;
function RenderFormPage(const FileName, PngName: string; PageIndex: Integer;
out Diagnostic: string): Boolean;
var
Doc: THPDFHeadlessDocument;
Options: THPDFHeadlessRenderOptions;
Info: THPDFHeadlessRenderResult;
begin
Result := False;
Diagnostic := '';
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
Options := THPDFHeadlessRenderOptions.Default; // RenderAnnotations = True, MaxDepth = 32
Options.Usage := hruPrint; // comme l'impression d'un visionneur : les widgets sans le drapeau Print sont sautés
try
// PageIndex est base zéro
Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
PngName, Options);
Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
Result := True;
except
on E: EHPDFHeadlessRenderError do
// p. ex. 'Widget field requires a name field type' ; pour un formulaire rempli, ne
// réessayez pas avec RenderAnnotations := False, qui lâche chaque widget et sa valeur
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
Le repli des champs de texte dessine les champs une ligne qui détiennent une valeur chaîne. Pour toute mise en page de texte plus riche, générez l'apparence au moment du remplissage côté Windows, où EnsureLoadedFieldAppearanceStream construit le flux /AP manquant avant que le fichier ne soit aplati ou expédié ; le travail Linux reçoit alors de vrais flux d'apparence, et le moteur headless nomme tout champ qu'il rencontre qui en demande encore un. Jugez pourtant la sortie Linux par le moteur Linux. RenderLoadedPageToBitmap dessine à travers le backend GDI Windows, qui ne lance jamais ce repli, si bien qu'une page qui y paraît correcte ne vous dit rien de ce que le périphérique Cairo produira du même fichier
Les bits de drapeaux de champ se comptent à partir de un
L'ISO 32000-1 §12.7.3.1 énonce que les positions de bits dans le mot /Ff courent de 1, à l'extrémité basse, à 32, et les tableaux 226, 228 et 230 utilisent tous cette numérotation. En Delphi, le bit N vaut donc 1 shl (N - 1). Décalez du numéro de bit lui-même et chaque test monte d'une position : 1 shl 16 écrit pour "radio, bit 16" teste en réalité le drapeau bouton poussoir, et 1 shl 18 écrit pour "combo, bit 18" teste Edit. Les deux masques faux sont de vrais drapeaux. Le code compile, tourne, et range les champs dans la mauvaise catégorie
| Drapeau | Tableau | Bit | Masque Delphi |
|---|---|---|---|
| Multiline | 228 (texte) | 13 | 1 shl 12 = 4096 |
| Password | 228 (texte) | 14 | 1 shl 13 = 8192 |
| Comb | 228 (texte) | 25 | 1 shl 24 = 16777216 |
| RichText | 228 (texte) | 26 | 1 shl 25 = 33554432 |
| Radio | 226 (bouton) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226 (bouton) | 17 | 1 shl 16 = 65536 |
| Combo | 230 (choix) | 18 | 1 shl 17 = 131072 |
uses
Classes, SysUtils, HPDFHeadlessDocument;
const
// L'ISO 32000-1 numérote les bits de Ff à partir de 1 (poids faible), donc le bit N vaut 1 shl (N - 1)
TxMultiline = 1 shl 12; // bit 13
TxPassword = 1 shl 13; // bit 14
TxFileSelect = 1 shl 20; // bit 21
TxDoNotScroll = 1 shl 23; // bit 24
TxComb = 1 shl 24; // bit 25
TxRichText = 1 shl 25; // bit 26
TxProducerLayout = TxMultiline or TxPassword or TxFileSelect or
TxDoNotScroll or TxComb or TxRichText;
procedure ListTextFieldsForProducerAppearance(const FileName: string;
Report: TStrings);
var
Doc: THPDFHeadlessDocument;
Field: THPDFHeadlessFormField;
I: Integer;
begin
Doc := THPDFHeadlessDocument.Create;
try
Doc.LoadFromFile(FileName);
for I := 0 to Doc.FormFieldCount - 1 do
begin
// FieldType, Flags et MaxLength arrivent déjà hérités des ancêtres
Field := Doc.GetFormField(I);
if Field.FieldType <> 'Tx' then
Continue;
if (Field.Flags and TxProducerLayout) <> 0 then
Report.Add(Format('%s: flags $%.8x, needs a producer /AP',
[Field.Name, Field.Flags]))
else if Field.MaxLength > 0 then
Report.Add(Format('%s: /MaxLen %d, needs a producer /AP',
[Field.Name, Field.MaxLength]))
else if Field.Value = '' then
Report.Add(Format('%s: blank, fill it or give it an /AP', [Field.Name]));
end;
finally
Doc.Free;
end;
end;
Le balayage lit les drapeaux au niveau du champ, qui est là où les producteurs les posent ; le moteur honore aussi un /Ff posé sur un widget séparé, alors traitez la liste comme une aide de planification et le diagnostic de rendu comme le dernier mot. Le contrôle du vide compte parce qu'un champ de texte sans /V ni /AP n'a rien à synthétiser : le rendu s'arrête avec "A text-field widget without a value has no appearance to synthesize", et le remède est une valeur ou une apparence avant que le fichier n'atteigne le moteur
Que signifie "Degenerate appearance geometry" ?
Cela signifie que le rectangle ou la boîte d'apparence n'a pas d'aire, et un /Rect inversé n'en est pas la cause. L'ISO 32000-1 §7.9.5 laisse un rectangle nommer deux coins diagonalement opposés quelconques, et sa note dit au logiciel de traitement d'être prêt à les normaliser. Un moteur naïf qui calcule la largeur comme x2 - x1 obtient un nombre négatif de [300 720 160 700] et rejette l'annotation ou reflète son apparence. HotPDF échange les coordonnées en ordre bas-gauche et haut-droite avant tout contrôle de taille, pour les faces synthétisées comme pour les flux /AP existants, si bien que le widget 11 ci-dessus se rend comme si ses coins étaient conventionnels. L'erreur n'est levée que pour une annotation qui a un flux d'apparence quand, après normalisation, le /Rect a une largeur ou une hauteur nulle, ou que le /BBox du flux se replie en ligne ou en point sous son /Matrix. Quand elle apparaît, videz le /Rect du widget avec le /BBox et le /Matrix du flux : l'un des trois a une étendue nulle
Un widget sans /AP et avec un /Rect d'aire nulle prend une autre route : le repli le saute et poursuit avec la page. L'ISO 32000-2 §12.5.2 exempte de l'exigence d'apparence seules les annotations Popup, Projection et Link et un rectangle qui se replie en un point unique. Un rectangle qui n'est qu'une ligne doit encore une apparence sous PDF 2.0 ; le moteur saute aussi celui-là, simplement parce qu'il n'y a pas d'aire à peindre
Quand le type de champ est introuvable
"Widget field requires a name field type" signifie que le moteur a grimpé depuis le widget aussi loin que les liens /Parent menaient, sans trouver de /FT. Le moteur part du tableau /Annots de la page, si bien que le lien /Parent montant est sa seule route vers le champ. Le chargement marche dans l'autre sens, vers le bas depuis le tableau /Fields de l'AcroForm à travers /Kids, c'est pourquoi LoadFromFile peut réussir sur un fichier dont la page échoue ensuite au rendu. Une cause typique : un widget listé dans les /Kids d'un champ et qui ne renvoie pas en arrière. Le tableau 188 de l'ISO 32000-1 donne là-dessus un peu de couverture : il exige /Parent sur un widget seulement quand le widget est l'un de plusieurs enfants et dit "absent otherwise", si bien qu'un écrivain qui le lit littéralement lâche le lien pour un champ avec un seul widget séparé. L'ISO 32000-2 a assoupli la même entrée en "optional otherwise". Nous écririons de toute façon /Parent sur chaque widget qui n'est pas fusionné avec son champ. L'ISO 32000-2 le permet, et c'est la seule manière dont un lecteur piloté par la page, le nôtre ou celui de n'importe qui, peut atteindre le champ. Si vous ne pouvez pas corriger le producteur, fusionner un widget solitaire dans son dictionnaire de champ donne le même résultat
Un widget qui renvoie à son parent et trouve un /DA quelque part en montant se rend correctement sans aucun /AP dans le moteur headless du HotPDF Delphi component, ce qui fait de l'arbre des champs, plus que des flux d'apparence, la première chose à bien faire dans un pipeline de formulaires Linux