Technischer Artikel

PDF-Formularfelder ohne /AP unter Linux rendern

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

AcroForm-Objektgraph für Headless-Rendering mit HotPDF in Delphi: Nur-Geometrie-Widgets klettern über /Parent zum Feld, das /FT, /V, /DA und /Q hält
Der nächste Wert entlang der /Parent-Kette gewinnt, und nur /DA und /Q fallen aufs AcroForm-Wörterbuch zurück, sobald die Kette ausgeht

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:

FeldSynthetisierte Appearance
Einzeiliger Text, /FT /TxDer String /V, gesetzt mit dem aufgelösten /DA und /Q innerhalb von /Rect
Checkbox, /FT /BtnRechteckiger Rand mit Eckenradien aus /Border, /MK /BG-Füllung und einem gezeichneten Vektor-Haken im aktiven Zustand
Radiobutton, Btn mit Bit 16Runder Rand mit gefülltem Punkt im aktiven Zustand
Pushbutton, Btn mit Bit 17Rechteckiger Rand mit der zentrierten /MK /CA-Beschriftung
Combobox, /FT /Ch mit Bit 18Der Anzeigetext des gewählten /Opt-Eintrags als zentrierte Beschriftung
Listbox, Ch mit Bit 18 gelöschtEine 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

Entscheidungsfluss im HotPDF-Headless-Renderer für Delphi: Ein vorhandenes /AP gewinnt stets, sonst wählt der aufgelöste Feldtyp ein synthetisiertes Gesicht
Temporäre Appearances werden nur gezeichnet, wenn /AP fehlt oder null ist, und das geladene Dokument selbst wird nie verändert
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

PDF-Feld-Flags nummerieren Bits ab eins in Delphi: Das Lineal zeigt, warum Radio 1 shl 15 ist und 1 shl 16 Pushbutton erreicht
Bit N des /Ff-Worts nimmt die Delphi-Maske 1 shl N minus eins, und jede um eins verschobene Maske ist ein echtes Flag des Nachbar-Bits
FlagTabelleBitDelphi-Maske
Multiline228 (Text)131 shl 12 = 4096
Password228 (Text)141 shl 13 = 8192
Comb228 (Text)251 shl 24 = 16777216
RichText228 (Text)261 shl 25 = 33554432
Radio226 (Button)161 shl 15 = 32768
Pushbutton226 (Button)171 shl 16 = 65536
Combo230 (Choice)181 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