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
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:
| Campo | Appearance sintetizzato |
|---|---|
Testo a riga singola, /FT /Tx | La stringa /V, composta con il /DA e il /Q risolti dentro /Rect |
Checkbox, /FT /Btn | Bordo 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 16 | Bordo circolare con un punto pieno quando attivo |
| Pushbutton, Btn con bit 17 | Bordo rettangolare con la didascalia /MK /CA centrata |
Combo box, /FT /Ch con bit 18 | Il testo di visualizzazione della voce /Opt selezionata come didascalia centrata |
| List box, Ch con bit 18 a zero | Una 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
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
| Flag | Tabella | Bit | Maschera Delphi |
|---|---|---|---|
| Multiline | 228 (testo) | 13 | 1 shl 12 = 4096 |
| Password | 228 (testo) | 14 | 1 shl 13 = 8192 |
| Comb | 228 (testo) | 25 | 1 shl 24 = 16777216 |
| RichText | 228 (testo) | 26 | 1 shl 25 = 33554432 |
| Radio | 226 (pulsante) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226 (pulsante) | 17 | 1 shl 16 = 65536 |
| Combo | 230 (scelta) | 18 | 1 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