Artículo técnico

El renderizador headless de HotPDF y la herencia de campos

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

Grafo de objetos AcroForm para el renderizado headless de HotPDF en Delphi: widgets de solo geometría suben por /Parent hasta el campo que guarda /FT, /V, /DA y /Q
gana el valor más cercano por la cadena /Parent, y solo /DA y /Q caen al diccionario AcroForm cuando la cadena se agota

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:

CampoApariencia sintetizada
Texto de una línea, /FT /TxLa cadena /V, compuesta con el /DA y el /Q resueltos dentro del /Rect
Casilla, /FT /BtnBorde 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 16Borde circular con un punto relleno cuando está activado
Botón de acción, Btn con bit 17Borde rectangular con la leyenda del /MK /CA centrada
Cuadro combinado, /FT /Ch con bit 18El texto visible de la entrada /Opt seleccionada como leyenda centrada
Cuadro de lista, Ch con bit 18 a ceroUna 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

Flujo de decisión en el renderizador headless de HotPDF para Delphi: un /AP presente siempre gana, si no, el tipo de campo resuelto elige una cara sintetizada
las apariencias temporales se dibujan solo cuando /AP falta o es null, y el documento cargado jamás se modifica
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

Los flags de campo PDF numeran los bits desde uno en Delphi: la regla muestra por qué radio es 1 shl 15 y 1 shl 16 alcanza a pushbutton
el bit N de la palabra /Ff toma la máscara Delphi 1 shl N menos uno, y toda máscara desplazada un puesto es un flag real del bit vecino
FlagTablaBitMáscara Delphi
Multiline228 (texto)131 shl 12 = 4096
Password228 (texto)141 shl 13 = 8192
Comb228 (texto)251 shl 24 = 16777216
RichText228 (texto)261 shl 25 = 33554432
Radio226 (botón)161 shl 15 = 32768
Pushbutton226 (botón)171 shl 16 = 65536
Combo230 (lista)181 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