Ein PDF-Formular mit Feldwerten, aber ohne Appearance-Streams kommt aus einem Linux-Renderlauf als leere Kästen zurück, mit falscher Schrift oder Ausrichtung, oder als EHPDFHeadlessRenderError mit dem Text "Widget field requires a name field type". HotPDFs Headless-Renderer zeichnet temporäre Appearances für Text-, Button- und Choice-Widgets und erbt /FT, /V, /DA und /Q über den Feldbaum
Wo ein Widget seine Feldeinstellungen aufbewahrt
Der meiste Ärger beginnt mit der Trennung zwischen einem Feld und seinen Widgets. Das Feld-Wörterbuch hält den logischen Teil eines Formularelements: Typ, Wert, Flags und Default-Appearance. Die Widget-Annotation hält das, was eine Seite braucht: /Rect, Rand, einen Zeiger auf ihre Seite. Ein Feld mit einem Widget ist gewöhnlich in ein einziges Wörterbuch verschmolzen. Muss derselbe Wert auf mehreren Seiten auftauchen, werden die Widgets Kinder des Felds und tragen oft nichts als Geometrie und eine /Parent-Referenz. ISO 32000-1 §12.7.3.1 macht /FT, /Ff, /V und /DV entlang dieser Kette vererbbar (Tabelle 220), und §12.7.3.3 fügt dem vererbbaren Set in Tabelle 222 nur zwei Variable-Text-Attribute hinzu: /DA, das erforderlich ist, aber von einem Vorfahren kommen darf, und /Q. Das Interactive-Form-Wörterbuch (Tabelle 218) liefert dokumentsweite Vorgaben für /DA und /Q, plus /DR, das Default-Ressourcen-Wörterbuch, in dem /DA-Schriftnamen nachgeschlagen werden. /DR steht nicht in Tabelle 222; es gehört zum Formular als Ganzes. Ein Renderer, der nur das Widget-Wörterbuch liest, findet in der Datei unten weder Typ noch Wert noch /DA und fällt auf seine eigene Standardschrift zurück
1 0 obj % Catalog
<< /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 % Terminalfeld: Typ, Wert und Appearance-Vorgaben wohnen hier
<< /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 auf Seite 1: nur Geometrie, Ecken oben rechts zuerst
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % Widget auf Seite 2: nur Geometrie
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF sucht jeden Feldeintrag am Widget und, wenn vergeblich, klettert es /Parent ein Wörterbuch nach dem anderen hinauf; der nächste Wert gewinnt, und nur /DA und /Q fallen aufs AcroForm-Wörterbuch zurück, sobald die Kette ausgeht. Erwartet für die Datei oben: Beide Widgets zeigen INV-2026-0417 rechtsbündig in 10-Punkt-Courier, dunkelblau, und das dokumentsweite /Helv 0 Tf kommt nie zum Zug, weil Objekt 10 sein eigenes /DA hat. Kommt ein Feld stattdessen in 12-Punkt-Schwarz-Helvetica heraus, ist der naheliegende Verdächtige eine Courier-Schrift, die auf der Linux-Kiste fehlt. Die Größe verrät es. Das ist, was HotPDF zeichnet, wenn kein /DA auftaucht, weder am Widget, an einem Vorfahren noch im AcroForm-Wörterbuch. Also: Widget ausgeben, seinem /Parent von Hand folgen und finden, wo der Producer das /DA abgelegt hat, etwa auf einem Feld-Wörterbuch, zu dem das Widget nie zurücklinkt
Schriftnamen aus dem /DA lösen sich gegen ein /DR am Widget oder einem Elternfeld, das AcroForm-/DR und die Schriften-Ressourcen der Seite, in dieser Reihenfolge. Ein /DR auf Feldebene liegt außerhalb von Tabelle 222, ist aber, wenn vorhanden, eindeutig, also akzeptiert HotPDF ihn. Der Aufstieg ist begrenzt: Eine Schleife im Baum stoppt mit "Widget field inheritance contains a cycle", ein /Parent, der kein Wörterbuch ist, mit "Widget Parent requires a dictionary", und eine Kette, tiefer als MaxDepth in den Render-Optionen (Standard 32), mit "Widget field inheritance depth budget exceeded". Die VCL-Komponente wendet dieselbe Vererbung an, wenn sie geerbte AcroForm-Werte liest und Felder zurücksetzt
Wie entscheidet der Headless-Renderer, was er zeichnet?
HPDFRenderHeadlessPagePNG und seine Dateivariante fahren den Fallback nur für ein sichtbares Widget, dessen /AP fehlt oder null ist; ISO 32000-1 §7.3.7 sagt, ein null-Wörterbuchwert ist dasselbe wie ein fehlender Eintrag, und der Renderer behandelt /AP null entsprechend. Eine vorhandene Normal-Appearance gewinnt stets, was auch /NeedAppearances sagt, also zeigt eine veraltete Appearance aus einer früheren Befüllung weiter, bis etwas sie neu erzeugt. Das geladene Dokument wird nie verändert. Was gezeichnet wird, hängt vom aufgelösten Feldtyp und den Flags ab:
| Feld | Synthetisierte Appearance |
|---|---|
Einzeiliger Text, /FT /Tx | Der String /V, gesetzt mit dem aufgelösten /DA und /Q innerhalb von /Rect |
Checkbox, /FT /Btn | Rechteckiger Rand mit Eckenradien aus /Border, /MK /BG-Füllung und einem gezeichneten Vektor-Haken im aktiven Zustand |
| Radiobutton, Btn mit Bit 16 | Runder Rand mit gefülltem Punkt im aktiven Zustand |
| Pushbutton, Btn mit Bit 17 | Rechteckiger Rand mit der zentrierten /MK /CA-Beschriftung |
Combobox, /FT /Ch mit Bit 18 | Der Anzeigetext des gewählten /Opt-Eintrags als zentrierte Beschriftung |
| Listbox, Ch mit Bit 18 gelöscht | Eine Zeile pro Option ab /TI, mit einem Hervorhebungsbalken hinter jeder gewählten Zeile |
Häkchen und Radio-Punkt sind Vektorpfade statt ZapfDingbats-Glyphen, wie viele Generatoren sie zeichnen, denn ein schmaler Linux-Container hat ZapfDingbats womöglich nicht installiert. Pushbutton- und Combobox-Gesichter decken sich mit dem, was HotPDFs VCL-Appearance-Generator für dasselbe Feld zeichnet, also sieht ein Formular gleich aus, welche Seite auch immer das Bild erzeugt hat. Eine Beschriftung nimmt Schrift, Größe und Farbe aus dem /DA; da das Beschriftungs-Layout sich nicht selbst einpasst, wird eine /DA-Größe von 0 zu 70 Prozent der Boxhöhe, begrenzt bei 12 Punkt. Der Checkbox- und Radio-Zustand kommt aus dem eigenen /AS des Widgets und fällt auf das geerbte /V zurück, und jeder Zustand außer Off zeichnet die Markierung. Geben Sie jedem Radio-Kind sein eigenes /AS. Das /V des Elternfelds nennt den gewählten Zustand für die ganze Gruppe und kann kein Kind vom anderen unterscheiden
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; // wie der Druck eines Viewers: Widgets ohne das Print-Flag werden übersprungen
try
// PageIndex ist nullbasiert
Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
PngName, Options);
Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
Result := True;
except
on E: EHPDFHeadlessRenderError do
// z. B. 'Widget field requires a name field type'; bei einem ausgefüllten Formular
// nicht mit RenderAnnotations := False erneut versuchen: Das wirft jedes Widget samt Wert weg
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
Der Textfeld-Fallback zeichnet einzeilige Felder, die einen String-Wert halten. Für jedes reichhaltigere Text-Layout erzeugen Sie die Appearance zur Befüllzeit auf der Windows-Seite, wo EnsureLoadedFieldAppearanceStream den fehlenden /AP-Stream baut, bevor die Datei flattiert oder ausgeliefert wird; der Linux-Job bekommt dann echte Appearance-Streams, und der Headless-Renderer nennt jedes Feld, dem er begegnet und das noch einen braucht. Beurteilen Sie Linux-Ausgabe aber beim Linux-Renderer. RenderLoadedPageToBitmap zeichnet über das Windows-GDI-Backend, das diesen Fallback nie fährt, also verrät eine Seite, die dort richtig aussieht, nichts darüber, was das Cairo-Device aus derselben Datei produziert
Feld-Flag-Bits werden ab eins nummeriert
ISO 32000-1 §12.7.3.1 stellt fest, dass die Bitpositionen im /Ff-Wort von 1 am niederwertigen Ende bis 32 laufen, und die Tabellen 226, 228 und 230 benutzen alle diese Nummerierung. In Delphi ist Bit N also 1 shl (N - 1). Verschiebt man um die Bitnummer selbst, rückt jeder Test eine Position nach oben: 1 shl 16, geschrieben für "Radio, Bit 16", prüft tatsächlich das Pushbutton-Flag, und 1 shl 18, geschrieben für "Combo, Bit 18", prüft Edit. Beide falschen Masken sind echte Flags. Der Code kompiliert, läuft, und sortiert Felder in die falsche Art
| Flag | Tabelle | Bit | Delphi-Maske |
|---|---|---|---|
| Multiline | 228 (Text) | 13 | 1 shl 12 = 4096 |
| Password | 228 (Text) | 14 | 1 shl 13 = 8192 |
| Comb | 228 (Text) | 25 | 1 shl 24 = 16777216 |
| RichText | 228 (Text) | 26 | 1 shl 25 = 33554432 |
| Radio | 226 (Button) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226 (Button) | 17 | 1 shl 16 = 65536 |
| Combo | 230 (Choice) | 18 | 1 shl 17 = 131072 |
uses
Classes, SysUtils, HPDFHeadlessDocument;
const
// ISO 32000-1 nummeriert Ff-Bits ab 1 (niederwertig), Bit N ist also 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 und MaxLength kommen bereits von Vorfahren geerbt an
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;
Der Scan liest Flags auf Feldebene, dorthin legen sie die Producer; der Renderer ehrt auch ein /Ff an einem separaten Widget, also nehmen Sie die Liste als Planungshilfe und die Render-Diagnose als letztes Wort. Die Leer-Prüfung zählt, weil ein Textfeld ohne /V und ohne /AP nichts zu synthetisieren hat: Der Renderlauf stoppt mit "A text-field widget without a value has no appearance to synthesize", und die Abhilfe ist ein Wert oder eine Appearance, bevor die Datei den Renderer erreicht
Was bedeutet "Degenerate appearance geometry"?
Es bedeutet, dass das Rechteck oder die Appearance-Box keine Fläche hat, und ein umgedrehtes /Rect ist nicht die Ursache. ISO 32000-1 §7.9.5 gestattet einem Rechteck, je zwei diagonal gegenüberliegende Ecken zu nennen, und seine Notiz heißt verarbeitende Software willkommen, auf Normalisierung vorbereitet zu sein. Ein naiver Renderer, der die Breite als x2 - x1 berechnet, bekommt aus [300 720 160 700] eine negative Zahl und weist die Annotation entweder zurück oder spiegelt ihre Appearance. HotPDF tauscht die Koordinaten in die Reihenfolge unten links und oben rechts, bevor irgendeine Größenprüfung läuft, für synthetisierte Gesichter wie für vorhandene /AP-Streams, also rendert Widget 11 oben, als wären seine Ecken konventionell. Der Fehler wird nur für eine Annotation mit Appearance-Stream geworfen, wenn nach der Normalisierung das /Rect null Breite oder Höhe hat oder das /BBox des Streams unter seiner /Matrix zu einer Linie oder einem Punkt schrumpft. Taucht er auf, geben Sie das /Rect des Widgets zusammen mit /BBox und /Matrix des Streams aus: Eines der drei hat eine null Ausdehnung
Ein Widget ohne /AP und mit flächenlosem /Rect nimmt einen anderen Weg: Der Fallback überspringt es und arbeitet auf der Seite weiter. ISO 32000-2 §12.5.2 nimmt von der Appearance-Pflicht nur Popup-, Projection- und Link-Annotationen aus sowie ein Rechteck, das zu einem einzigen Punkt schrumpft. Ein Rechteck, das bloß eine Linie ist, schuldet unter PDF 2.0 weiterhin eine Appearance; der Renderer überspringt auch dieses, schlicht weil es keine Fläche zu malen gibt
Wenn der Feldtyp nicht zu finden ist
"Widget field requires a name field type" heißt, der Renderer ist vom Widget so weit geklettert, wie /Parent-Links reichten, und hat kein /FT gefunden. Der Renderer startet beim /Annots-Array der Seite, also ist der aufsteigende /Parent-Link sein einziger Weg zum Feld. Das Laden läuft den anderen Weg, hinab vom AcroForm-/Fields-Array durch /Kids, weshalb LoadFromFile an einer Datei gelingen kann, deren Seite später beim Rendern scheitert. Eine typische Ursache ist ein Widget, das in den /Kids eines Felds steht und nicht zurückzeigt. ISO 32000-1 Tabelle 188 gibt dafür etwas Deckung: Sie verlangt /Parent an einem Widget nur, wenn das Widget eines von mehreren Kindern ist, und sagt "absent otherwise", also streicht ein Writer, der sie wörtlich liest, den Link bei einem Feld mit einem separaten Widget. ISO 32000-2 hat denselben Eintrag zu "optional otherwise" gelockert. Wir würden ohnehin /Parent an jedes Widget schreiben, das nicht mit seinem Feld verschmolzen ist. ISO 32000-2 erlaubt es, und es ist der einzige Weg, wie ein seitengetriebener Reader, unserer wie der eines jeden anderen, das Feld erreichen kann. Lässt sich der Producer nicht beheben, verschafft auch das Verschmelzen eines einsamen Widgets in sein Feld-Wörterbuch dasselbe Ergebnis
Ein Widget, das zu seinem Elternteil zeigt und unterwegs irgendwo ein /DA findet, rendert im Headless-Renderer der HotPDF-Delphi-Komponente korrekt, ganz ohne /AP — was den Feldbaum, mehr noch als die Appearance-Streams, zur ersten Sache macht, die man in einer Linux-Formular-Pipeline richtig hinbekommen muss