Un formulario PDF con valores de campo pero sin flujos de apariencia puede salir de un trabajo de renderizado Linux como cuadros vacíos, con la fuente o la alineación equivocadas, o con un EHPDFHeadlessRenderError con el texto "Widget field requires a name field type". El renderizador headless de HotPDF dibuja apariencias temporales para widgets de texto, botón y lista, heredando /FT, /V, /DA y /Q por el árbol de campos
Dónde guarda la configuración de campo un widget
Casi todo el problema arranca con la división entre un campo y sus widgets. El diccionario de campo guarda la parte lógica de un control de formulario: su tipo, valor, flags y apariencia por defecto. La anotación widget guarda lo que una página necesita: /Rect, borde, un puntero a su página. Un campo con un widget suele ir fusionado en un único diccionario. Cuando el mismo valor tiene que aparecer en varias páginas, los widgets pasan a ser hijos del campo y a menudo no llevan nada más que geometría y una referencia /Parent. La ISO 32000-1 §12.7.3.1 hace heredables /FT, /Ff, /V y /DV por esa cadena (Tabla 220), y la §12.7.3.3 añade solo dos atributos de texto variable al conjunto heredable de la Tabla 222: /DA, que es obligatorio pero puede venir de un ancestro, y /Q. El diccionario de formulario interactivo (Tabla 218) aporta valores por defecto para todo el documento de /DA y /Q, más /DR, el diccionario de recursos por defecto en el que se resuelven los nombres de fuente del /DA. El /DR no está en la Tabla 222; pertenece al formulario como conjunto. Un renderizador que solo lee el diccionario del widget no encuentra tipo, valor ni /DA en el fichero de abajo y cae en su fuente por defecto
1 0 obj % catálogo
<< /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 terminal: el tipo, el valor y los valores por defecto de apariencia viven aquí
<< /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 en la página 1: solo geometría, esquinas dadas primero arriba-derecha
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj % widget en la página 2: solo geometría
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
/Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj
HotPDF busca cada entrada de campo en el widget y, si no está, sube por /Parent un diccionario cada vez; gana el valor más cercano, y solo /DA y /Q caen al diccionario AcroForm cuando la cadena se agota. Esperado para el fichero de arriba: ambos widgets muestran INV-2026-0417 alineado a la derecha en Courier de 10 puntos, azul oscuro, y el /Helv 0 Tf de todo el documento nunca entra en juego porque el objeto 10 tiene su propio /DA. Cuando un campo sale en Helvetica negra de 12 puntos, el sospechoso obvio es una fuente Courier ausente en la máquina Linux. El tamaño la delata. Eso es lo que HotPDF dibuja cuando ningún /DA aparece ni en el widget, ni en ancestro alguno ni en el diccionario AcroForm. Así que vuelca el widget, sigue su /Parent a mano y encuentra dónde puso el /DA el productor, por ejemplo en un diccionario de campo al que el widget nunca enlaza de vuelta
Los nombres de fuente del /DA se resuelven contra un /DR en el widget o en un campo padre, el /DR del AcroForm y los recursos de fuente de la página, en ese orden. Un /DR a nivel de campo está fuera de la Tabla 222, pero no deja lugar a dudas cuando está, así que HotPDF lo acepta. El ascenso tiene límite: un bucle en el árbol se detiene con "Widget field inheritance contains a cycle", un /Parent que no sea diccionario con "Widget Parent requires a dictionary", y una cadena más profunda que MaxDepth en las opciones de renderizado (32 por defecto) con "Widget field inheritance depth budget exceeded". El componente VCL aplica la misma herencia cuando lee valores AcroForm heredados y restablece campos
¿Cómo decide el renderizador headless qué dibujar?
HPDFRenderHeadlessPagePNG y su variante a fichero ejecutan el fallback solo para un widget visible cuyo /AP falte o sea null; la ISO 32000-1 §7.3.7 dice que un valor de diccionario null equivale a una entrada ausente, y el renderizador trata el /AP null así. Una apariencia normal presente siempre gana, diga lo que diga /NeedAppearances, de modo que una apariencia obsoleta de un relleno anterior sigue mostrándose hasta que algo la regenere. El documento cargado nunca se modifica. Lo que se dibuja depende del tipo de campo resuelto y de sus flags:
| Campo | Apariencia sintetizada |
|---|---|
Texto de una línea, /FT /Tx | La cadena /V, compuesta con el /DA y el /Q resueltos dentro del /Rect |
Casilla, /FT /Btn | Borde rectangular con radios de esquina del /Border, relleno del /MK /BG, y una marca vectorial trazada cuando está activada |
| Botón de opción, Btn con bit 16 | Borde circular con un punto relleno cuando está activado |
| Botón de acción, Btn con bit 17 | Borde rectangular con la leyenda del /MK /CA centrada |
Cuadro combinado, /FT /Ch con bit 18 | El texto visible de la entrada /Opt seleccionada como leyenda centrada |
| Cuadro de lista, Ch con bit 18 a cero | Una fila por opción empezando en /TI, con una banda de resaltado detrás de cada fila seleccionada |
La marca de verificación y el punto del botón de opción son trazados vectoriales y no glifos ZapfDingbats, como los dibujan muchos generadores, porque un contenedor Linux ligero puede no tener ZapfDingbats instalado. Las caras de botón de acción y de cuadro combinado coinciden con lo que dibuja el generador de apariencias VCL de HotPDF para el mismo campo, de modo que un formulario se ve igual produzca la imagen el lado que sea. Una leyenda toma su fuente, tamaño y color del /DA; como la composición de la leyenda no se autoajusta, un tamaño 0 del /DA pasa a ser el 70 por ciento de la altura de la caja, con tope de 12 puntos. El estado de casilla y botón de opción sale del /AS del propio widget, con repliegue al /V heredado, y cualquier estado distinto de Off dibuja la marca. Da a cada hijo de botón de opción su propio /AS. El /V del padre nombra el estado seleccionado del grupo entero y no distingue a un hijo de otro
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; // como la impresión de un visualizador: se saltan los widgets sin el flag Print
try
// PageIndex empieza en cero
Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
PngName, Options);
Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
Result := True;
except
on E: EHPDFHeadlessRenderError do
// p. ej. 'Widget field requires a name field type'; para un formulario relleno, no
// reintentes con RenderAnnotations := False, que suelta cada widget y su valor
Diagnostic := E.Message;
end;
finally
Doc.Free;
end;
end;
El fallback de campo de texto dibuja los campos de una línea que guardan un valor de cadena. Para cualquier composición de texto más rica, genera la apariencia en el momento del relleno en el lado Windows, donde EnsureLoadedFieldAppearanceStream construye el flujo /AP ausente antes de que el fichero se aplane o se envíe; el trabajo Linux recibe entonces flujos de apariencia de verdad, y el renderizador headless nombra cualquier campo que se encuentre y aún necesite uno. Juzga la salida Linux por el renderizador Linux, eso sí. RenderLoadedPageToBitmap dibuja a través del backend GDI de Windows, que nunca ejecuta este fallback, así que una página que allí se ve bien no te dice nada sobre lo que el dispositivo Cairo producirá del mismo fichero
Los bits de flags de campo se numeran desde uno
La ISO 32000-1 §12.7.3.1 declara que las posiciones de bit de la palabra /Ff van de 1 en el extremo bajo a 32, y las Tablas 226, 228 y 230 usan todas esa numeración. En Delphi, el bit N es por tanto 1 shl (N - 1). Desplaza por el número de bit sin más y toda comprobación sube una posición: un 1 shl 16 escrito para "radio, bit 16" comprueba en realidad el flag de botón de acción, y un 1 shl 18 escrito para "combo, bit 18" comprueba Edit. Las dos máscaras equivocadas son flags reales. El código compila, se ejecuta, y clasifica los campos en la categoría equivocada
| Flag | Tabla | Bit | Máscara Delphi |
|---|---|---|---|
| Multiline | 228 (texto) | 13 | 1 shl 12 = 4096 |
| Password | 228 (texto) | 14 | 1 shl 13 = 8192 |
| Comb | 228 (texto) | 25 | 1 shl 24 = 16777216 |
| RichText | 228 (texto) | 26 | 1 shl 25 = 33554432 |
| Radio | 226 (botón) | 16 | 1 shl 15 = 32768 |
| Pushbutton | 226 (botón) | 17 | 1 shl 16 = 65536 |
| Combo | 230 (lista) | 18 | 1 shl 17 = 131072 |
uses
Classes, SysUtils, HPDFHeadlessDocument;
const
// La ISO 32000-1 numera los bits de Ff desde 1 (extremo bajo), así que el bit N es 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 y MaxLength llegan ya heredados de los ancestros
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;
El barrido lee los flags a nivel de campo, que es donde los ponen los productores; el renderizador también tiene en cuenta un /Ff en un widget separado, así que trata la lista como una ayuda de planificación y el diagnóstico del renderizado como la última palabra. La comprobación de vacíos importa porque un campo de texto sin /V y sin /AP no tiene nada que sintetizar: el renderizado se detiene con "A text-field widget without a value has no appearance to synthesize", y la cura es un valor o una apariencia antes de que el fichero llegue al renderizador
¿Qué significa "Degenerate appearance geometry"?
Significa que el rectángulo o la caja de apariencia no tiene área, y un /Rect invertido no es la causa. La ISO 32000-1 §7.9.5 deja que un rectángulo nombre dos esquinas diagonalmente opuestas cualesquiera, y su nota manda al software de procesamiento estar preparado para normalizarlas. Un renderizador ingenuo que calcula el ancho como x2 - x1 obtiene un número negativo de [300 720 160 700] y o rechaza la anotación o espeja su apariencia. HotPDF intercambia las coordenadas al orden inferior-izquierda y superior-derecha antes de cualquier comprobación de tamaño, tanto para caras sintetizadas como para flujos /AP existentes, así que el widget 11 de arriba se renderiza como si sus esquinas fueran convencionales. El error solo se lanza para una anotación con flujo de apariencia cuando, tras la normalización, el /Rect tiene ancho o alto cero, o el /BBox del flujo colapsa a una línea o a un punto bajo su /Matrix. Cuando aparezca, vuelca el /Rect del widget junto con el /BBox y el /Matrix del flujo: uno de los tres tiene extensión cero
Un widget sin /AP y con un /Rect de área cero toma otra ruta: el fallback lo salta y sigue con la página. La ISO 32000-2 §12.5.2 exime del requisito de apariencia solo a las anotaciones Popup, Projection y Link y a un rectángulo que colapse a un único punto. Un rectángulo que es mera línea sigue debiendo una apariencia bajo PDF 2.0; el renderizador también se lo salta, sencillamente porque no hay área que pintar
Cuando no se encuentra el tipo de campo
"Widget field requires a name field type" significa que el renderizador subió desde el widget hasta donde llegaron los enlaces /Parent y no encontró ningún /FT. El renderizador parte del array /Annots de la página, así que el enlace /Parent ascendente es su única ruta hacia el campo. La carga camina al revés, hacia abajo desde el array /Fields del AcroForm por los /Kids, por eso LoadFromFile puede tener éxito en un fichero cuya página luego falla al renderizar. Una causa típica es un widget listado en los /Kids de un campo que no apunta de vuelta. La Tabla 188 de la ISO 32000-1 da cierta cobertura: exige /Parent en un widget solo cuando el widget es uno de varios hijos y dice "absent otherwise", de modo que un escritor que la lea al pie de la letra suelta el enlace para un campo con un único widget separado. La ISO 32000-2 relajó esa misma entrada a "optional otherwise". Nosotros escribiríamos /Parent en cada widget que no vaya fusionado con su campo, de todos modos. La ISO 32000-2 lo permite, y es la única manera de que un lector guiado por páginas, el nuestro o el de cualquiera, alcance el campo. Si no puedes arreglar al productor, fusionar un widget solitario en el diccionario de su campo da el mismo resultado
Un widget que enlaza a su padre y encuentra un /DA en algún punto del ascenso se renderiza bien sin ningún /AP en el renderizador headless del HotPDF Delphi component, lo que convierte al árbol de campos, más que a los flujos de apariencia, en lo primero que hay que dejar bien en un pipeline de formularios Linux