Articolo tecnico

HotPDF headless: widget senza aspetto ed ereditarietà

Un form PDF con valori nei campi ma senza appearance stream può uscire da un lavoro di rendering su Linux come caselle vuote, font o allineamento sbagliato, o come un EHPDFHeadlessRenderError che dice "Widget field requires a name field type". Il renderer headless di HotPDF disegna appearance temporanei per i widget di testo, pulsanti e scelte, ereditando /FT, /V, /DA e /Q attraverso il field tree

Dove un widget conserva le impostazioni del suo campo

Gran parte dei guai nasce dalla divisione tra un campo e i suoi widget. Il dizionario del campo tiene la parte logica di un controllo del form: tipo, valore, flag e aspetto predefinito. L'annotazione widget tiene ciò che serve alla pagina: /Rect, bordo, un puntatore alla sua pagina. Un campo con un solo widget di solito è fuso in un dizionario unico. Quando lo stesso valore deve comparire su più pagine, i widget diventano figli del campo e spesso non portano altro che geometria e un riferimento /Parent. ISO 32000-1 §12.7.3.1 rende /FT, /Ff, /V e /DV ereditabili lungo quella catena (Tabella 220), e il §12.7.3.3 aggiunge solo due attributi di testo variabile all'insieme ereditabile della Tabella 222: /DA, obbligatorio ma che può venire da un antenato, e /Q. Il dizionario del form interattivo (Tabella 218) fornisce i valori predefiniti a livello di documento per /DA e /Q, più /DR, il dizionario di risorse predefinito in cui si risolvono i nomi di font del /DA. /DR non è nella Tabella 222; appartiene al form nel suo complesso. Un renderer che legge solo il dizionario del widget non trova tipo, valore né /DA nel file qui sotto e ripiega sul suo font predefinito

1 0 obj  % catalogo
<< /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  % campo terminale: tipo, valore e aspetto predefinito stanno qui
<< /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 sulla pagina 1: solo geometria, angoli elencati partendo da alto a destra
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj  % widget sulla pagina 2: solo geometria
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj

HotPDF cerca ogni voce di campo sul widget e, in mancanza, risale il /Parent un dizionario alla volta; vince il valore più vicino, e solo /DA e /Q ripiegano sul dizionario AcroForm finita la catena. Atteso per il file qui sopra: entrambi i widget mostrano INV-2026-0417 allineato a destra in Courier a 10 punti, blu scuro, e il /Helv 0 Tf a livello di documento non entra mai in gioco perché l'oggetto 10 ha il suo /DA. Quando invece un campo esce in Helvetica nera a 12 punti, il sospetto ovvio è un font Courier assente dalla macchina Linux. A tradirlo è la dimensione. È ciò che HotPDF disegna quando nessun /DA salta fuori sul widget, su un antenato qualsiasi o nel dizionario AcroForm. Quindi dumpate il widget, seguitene il /Parent a mano e trovate dove il produttore ha messo il /DA, per esempio su un dizionario di campo a cui il widget non punta mai indietro

Grafo di oggetti AcroForm per il rendering headless di HotPDF in Delphi: widget con sola geometria risalgono il /Parent verso il campo che tiene /FT, /V, /DA e /Q
Vince il valore più vicino lungo la catena /Parent, e solo /DA e /Q ripiegano sul dizionario AcroForm finita la catena

I nomi di font del /DA si risolvono contro un /DR sul widget o su un campo padre, l'/DR dell'AcroForm e le risorse font della pagina, in quest'ordine. Un /DR a livello di campo sta fuori dalla Tabella 222, ma quando c'è non lascia ambiguità, così HotPDF lo accetta. La risalita è limitata: un ciclo nell'albero si ferma con "Widget field inheritance contains a cycle", un /Parent che non è un dizionario con "Widget Parent requires a dictionary", e una catena più profonda di MaxDepth nelle opzioni di rendering (32 per impostazione predefinita) con "Widget field inheritance depth budget exceeded". Il componente VCL applica la stessa ereditarietà quando legge i valori AcroForm ereditati e azzera i campi

Come decide il renderer headless cosa disegnare?

HPDFRenderHeadlessPagePNG e la sua variante su file eseguono il ripiego solo per un widget visibile il cui /AP manca o è null; ISO 32000-1 §7.3.7 dice che un valore null in un dizionario equivale a una voce assente, e il renderer tratta /AP null proprio così. Un appearance normale presente vince sempre, qualunque cosa dica /NeedAppearances, così un appearance stantio di un riempimento precedente continua a vedersi finché qualcosa non lo rigenera. Il documento caricato non viene mai modificato. Ciò che viene disegnato dipende dal tipo di campo risolto e dai flag:

CampoAppearance sintetizzato
Testo a riga singola, /FT /TxLa stringa /V, composta con il /DA e il /Q risolti dentro /Rect
Checkbox, /FT /BtnBordo rettangolare con raggi degli angoli da /Border, riempimento /MK /BG, e un segno di spunta vettoriale a tratto quando attivo
Radio button, Btn con bit 16Bordo circolare con un punto pieno quando attivo
Pushbutton, Btn con bit 17Bordo rettangolare con la didascalia /MK /CA centrata
Combo box, /FT /Ch con bit 18Il testo di visualizzazione della voce /Opt selezionata come didascalia centrata
List box, Ch con bit 18 a zeroUna riga per opzione a partire da /TI, con una banda di evidenziazione dietro ogni riga selezionata

Il segno di spunta e il punto del radio sono percorsi vettoriali anziché glifi ZapfDingbats, come li disegnano tanti generatori, perché un container Linux minimale può non avere ZapfDingbats installato. Le facce di pushbutton e combo box rispecchiano ciò che il generatore di appearance VCL di HotPDF disegna per lo stesso campo, così un form si presenta uguale indipendentemente dal lato che ha prodotto l'immagine. Una didascalia prende font, dimensione e colore dal /DA; visto che la composizione della didascalia non si adatta da sola, una dimensione /DA di 0 diventa il 70 percento dell'altezza della casella, con tetto a 12 punti. Lo stato di checkbox e radio arriva dal /AS del widget stesso, con ripiego sul /V ereditato, e qualunque stato diverso da Off disegna il segno. Date a ogni figlio radio il suo /AS. Il /V del padre nomina lo stato selezionato per l'intero gruppo e non sa distinguere un figlio dall'altro

Flusso decisionale nel renderer headless di HotPDF per Delphi: un /AP presente vince sempre, altrimenti il tipo di campo risolto sceglie una faccia sintetizzata
Gli appearance temporanei vengono disegnati solo quando /AP manca o è null, e il documento caricato non viene mai modificato
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;  // come la stampa di un viewer: i widget senza flag Print vengono saltati
    try
      // PageIndex è a base zero
      Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
        PngName, Options);
      Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
      Result := True;
    except
      on E: EHPDFHeadlessRenderError do
        // per es. Widget field requires a name field type; per un form compilato non
        // riprovate con RenderAnnotations := False, che butta via ogni widget e il suo valore
        Diagnostic := E.Message;
    end;
  finally
    Doc.Free;
  end;
end;

Il ripiego per i campi di testo disegna i campi a riga singola che tengono un valore stringa. Per qualunque composizione del testo più ricca, generate l'appearance al momento del riempimento sul lato Windows, dove EnsureLoadedFieldAppearanceStream costruisce lo stream /AP mancante prima che il file venga appiattito o spedito; il lavoro Linux riceve così appearance stream veri, e il renderer headless nomina qualunque campo incontri che ne abbia ancora bisogno. Giudicate però l'output Linux col renderer Linux. RenderLoadedPageToBitmap disegna attraverso il backend GDI di Windows, che non esegue mai questo ripiego, così una pagina che lì sembra a punto non vi dice nulla su ciò che il device Cairo produrrà dallo stesso file

I bit dei flag di campo si contano da uno

ISO 32000-1 §12.7.3.1 dichiara che le posizioni dei bit nella parola /Ff corrono da 1 all'estremità bassa fino a 32, e le Tabelle 226, 228 e 230 usano tutte quella numerazione. In Delphi, il bit N è quindi 1 shl (N - 1). Spostate del numero del bit stesso e ogni test sale di una posizione: 1 shl 16 scritto per "radio, bit 16" prova in realtà il flag pushbutton, e 1 shl 18 scritto per "combo, bit 18" prova Edit. Entrambe le maschere sbagliate sono flag veri. Il codice compila, gira, e ordina i campi nel tipo sbagliato

I flag di campo PDF numerano i bit da uno in Delphi: il righello mostra perché radio è 1 shl 15 e 1 shl 16 arriva a pushbutton
Il bit N della parola /Ff prende la maschera Delphi 1 shl N meno uno, e ogni maschera fuori di uno è un flag vero del bit vicino
FlagTabellaBitMaschera Delphi
Multiline228 (testo)131 shl 12 = 4096
Password228 (testo)141 shl 13 = 8192
Comb228 (testo)251 shl 24 = 16777216
RichText228 (testo)261 shl 25 = 33554432
Radio226 (pulsante)161 shl 15 = 32768
Pushbutton226 (pulsante)171 shl 16 = 65536
Combo230 (scelta)181 shl 17 = 131072
uses
  Classes, SysUtils, HPDFHeadlessDocument;

const
  // ISO 32000-1 numera i bit di Ff da 1 (estremo basso), quindi il bit N è 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 e MaxLength arrivano già ereditati dagli antenati
      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;

La scansione legge i flag a livello di campo, che è dove li mettono i produttori; il renderer onora anche un /Ff su un widget separato, quindi trattate l'elenco come un aiuto di pianificazione e il messaggio diagnostico del rendering come ultima parola. Il controllo sul vuoto conta perché un campo di testo senza /V e senza /AP non ha niente da sintetizzare: il rendering si ferma con "A text-field widget without a value has no appearance to synthesize", e la cura è un valore o un appearance prima che il file arrivi al renderer

Cosa significa "Degenerate appearance geometry"?

Significa che il rettangolo o la casella di appearance non ha area, e un /Rect invertito non è la causa. ISO 32000-1 §7.9.5 permette a un rettangolo di nominare due qualunque angoli opposti in diagonale, e la sua nota dice al software di elaborazione di essere pronto a normalizzarli. Un renderer ingenuo che calcola la larghezza come x2 - x1 ricava un numero negativo da [300 720 160 700] e o respinge l'annotazione o specchia il suo appearance. HotPDF scambia le coordinate in ordine basso-sinistra e alto-destra prima di ogni controllo di dimensione, sia per le facce sintetizzate sia per gli stream /AP esistenti, così il widget 11 qui sopra viene renderizzato come se i suoi angoli fossero convenzionali. L'errore viene sollevato solo per un'annotazione che ha uno stream di appearance quando, dopo la normalizzazione, il /Rect ha larghezza o altezza zero, o lo /BBox dello stream collassa in una linea o un punto sotto la sua /Matrix. Quando compare, dumpate il /Rect del widget insieme allo /BBox e alla /Matrix dello stream: uno dei tre ha estensione zero

Un widget senza /AP e con un /Rect di area zero prende una strada diversa: il ripiego lo salta e va avanti con la pagina. ISO 32000-2 §12.5.2 esenta dal requisito di appearance solo le annotazioni Popup, Projection e Link e un rettangolo che collassa in un punto singolo. Un rettangolo che è solo una linea deve ancora un appearance sotto PDF 2.0; il renderer salta anche quello, semplicemente perché non c'è area da dipingere

Quando il tipo di campo non si trova

"Widget field requires a name field type" significa che il renderer è risalito dal widget fin dove arrivavano i link /Parent senza trovare alcun /FT. Il renderer parte dall'array /Annots della pagina, così il link /Parent verso l'alto è la sua sola strada verso il campo. Il caricamento cammina nell'altra direzione, dall'array /Fields dell'AcroForm giù per i /Kids, ecco perché LoadFromFile può riuscire su un file la cui pagina poi non si renderizza. Una causa tipica è un widget elencato nei /Kids di un campo che non punta indietro. La Tabella 188 di ISO 32000-1 dà qualche copertura al proposito: richiede /Parent su un widget solo quando il widget è uno di più figli e dice "absent otherwise", così un writer che la legge alla lettera butta via il link per un campo con un widget separato. ISO 32000-2 ha rilassato la stessa voce in "optional otherwise". Noi scriveremmo comunque /Parent su ogni widget che non è fuso col suo campo. ISO 32000-2 lo consente, ed è l'unico modo in cui un lettore guidato dalla pagina, il nostro o di chiunque altro, può raggiungere il campo. Se non potete sistemare il produttore, fondere un widget solitario nel dizionario del suo campo dà lo stesso risultato

Un widget che punta al padre e trova un /DA da qualche parte lungo la risalita si renderizza correttamente senza alcun /AP nel renderer headless del componente HotPDF per Delphi, il che fa del field tree, più che degli appearance stream, la prima cosa da sistemare in una pipeline di form su Linux