Artículo técnico

Widget sin apariencia: herencia de campos en HotPDF

Un formulario PDF con valores de campo pero sin flujos de apariencia puede salir de un trabajo de render Linux como cajas en blanco, con la fuente o la alineación equivocadas, o con un EHPDFHeadlessRenderError que dice "Widget field requires a name field type". El renderer headless de HotPDF dibuja apariencias temporales para widgets de texto, botón y elección, heredando /FT, /V, /DA y /Q por el árbol de campos

Dónde guarda un widget su configuración de campo

La mayoría de los problemas empieza 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, banderas 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 normalmente se fusiona en un solo diccionario. Cuando el mismo valor tiene que aparecer en varias páginas, los widgets se vuelven hijos del campo y a menudo no cargan más que geometría y una referencia /Parent. ISO 32000-1 §12.7.3.1 hace a /FT, /Ff, /V y /DV heredables por esa cadena (Tabla 220), y §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 donde se resuelven los nombres de fuentes del /DA. /DR no está en la Tabla 222; pertenece al formulario como un todo. Un renderer que lee solo el diccionario widget no encuentra tipo, valor ni /DA en el archivo de abajo y retrocede a 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: tipo, valor y 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 arriba a la derecha primero
<< /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 del campo en el widget y, si no está, sube por /Parent un diccionario a la vez; gana el valor más cercano, y solo /DA y /Q recurren al diccionario AcroForm cuando la cadena se agota. Resultado esperado para el archivo 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 que falta en la máquina Linux. El tamaño lo delata. Eso es lo que HotPDF dibuja cuando ningún /DA aparece en el widget, en ningún ancestro ni en el diccionario AcroForm. Así que vuelque el widget, siga su /Parent a mano y encuentre dónde puso el productor el /DA, por ejemplo en un diccionario de campo al que el widget nunca apunta de vuelta

Grafo de objetos AcroForm para el renderizado headless de HotPDF en Delphi: los 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 recurren al diccionario AcroForm cuando la cadena se agota

Los nombres de fuentes del /DA se resuelven contra un /DR en el widget o en un campo padre, el /DR del AcroForm y los recursos de fuentes de la página, en ese orden. Un /DR a nivel de campo está fuera de la Tabla 222, pero no es ambiguo cuando está presente, así que HotPDF lo acepta. El ascenso tiene límite: un ciclo en el árbol se detiene con "Widget field inheritance contains a cycle", un /Parent que no es diccionario con "Widget Parent requires a dictionary", y una cadena más profunda que MaxDepth en las opciones de render (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 renderer headless qué dibujar?

HPDFRenderHeadlessPagePNG y su variante de archivo ejecutan el fallback solo para un widget visible cuyo /AP falta o es null; ISO 32000-1 §7.3.7 dice que un valor de diccionario null es lo mismo que una entrada ausente, y el renderer trata /AP null de esa manera. Una apariencia normal que está presente siempre gana, diga lo que diga /NeedAppearances, así que una apariencia vieja de un llenado anterior sigue mostrándose hasta que algo la regenere. El documento cargado nunca se modifica. Lo que se dibuja depende del tipo de campo y las banderas resueltas:

CampoApariencia sintetizada
Texto de una línea, /FT /TxLa cadena /V, compuesta con el /DA y el /Q resueltos dentro de /Rect
Casilla de verificación, /FT /BtnBorde rectangular con radios de esquina de /Border, relleno /MK /BG y una marca de verificación vectorial trazada cuando está activada
Botón de opción, Btn con el bit 16Borde circular con un punto relleno cuando está activo
Pushbutton, Btn con el bit 17Borde rectangular con la leyenda /MK /CA centrada
Cuadro combinado, /FT /Ch con el bit 18El texto visible de la entrada /Opt seleccionada como leyenda centrada
Cuadro de lista, Ch con el bit 18 en ceroUna fila por opción empezando en /TI, con una banda de resalte detrás de cada fila seleccionada

La marca de verificación y el punto del botón de opción son trazados vectoriales en lugar de glifos ZapfDingbats, la manera en que muchos generadores los dibujan, porque un contenedor Linux ligero puede no tener ZapfDingbats instalado. Las caras de pushbutton y de cuadro combinado coinciden con lo que el generador de apariencias VCL de HotPDF dibuja para el mismo campo, así que un formulario se ve igual sin importar qué lado produjo la imagen. Una leyenda toma su fuente, tamaño y color del /DA; como la composición de la leyenda no se autoajusta, un tamaño /DA de 0 se vuelve 70 por ciento de la altura de la caja, con tope de 12 puntos. El estado de casilla y de botón de opción viene del /AS del propio widget, recurriendo al /V heredado, y cualquier estado distinto de Off dibuja la marca. Dele a cada hijo de botón de opción su propio /AS. El /V del padre nombra el estado seleccionado para el grupo entero y no puede distinguir a un hijo de otro

Flujo de decisión en el renderer 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 en sí nunca 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 visor: se saltan los widgets sin la bandera Print
    try
      // PageIndex es de base 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 lleno, no
        // reintente con RenderAnnotations := False, que suelta cada widget y su valor
        Diagnostic := E.Message;
    end;
  finally
    Doc.Free;
  end;
end;

El fallback del campo de texto dibuja campos de una línea que guardan un valor de cadena. Para cualquier composición de texto más rica, genere la apariencia al momento del llenado del lado Windows, donde EnsureLoadedFieldAppearanceStream construye el flujo /AP faltante antes de que el archivo se aplane o se envíe; el trabajo Linux entonces recibe flujos de apariencia reales, y el renderer headless nombra cualquier campo que encuentre que todavía necesite uno. Eso sí: juzgue la salida Linux por el renderer Linux. RenderLoadedPageToBitmap dibuja a través del backend GDI de Windows, que nunca ejecuta este fallback, así que una página que se ve bien ahí no le dice nada sobre lo que el dispositivo Cairo va a producir del mismo archivo

Los bits de bandera de campo se numeran desde uno

ISO 32000-1 §12.7.3.1 establece que las posiciones de bit en la palabra /Ff corren desde 1 en el extremo bajo hasta 32, y las Tablas 226, 228 y 230 usan esa numeración. En Delphi, el bit N es por tanto 1 shl (N - 1). Desplace por el número de bit en sí y cada prueba se mueve una posición arriba: un 1 shl 16 escrito para "radio, bit 16" en realidad prueba la bandera de pushbutton, y un 1 shl 18 escrito para "combo, bit 18" prueba Edit. Ambas máscaras equivocadas son banderas reales. El código compila, corre, y clasifica los campos en el tipo equivocado

Las banderas de campo PDF numeran 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 cada máscara corrida por uno es una bandera real del bit vecino
BanderaTablaBitMá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 (elección)181 shl 17 = 131072
uses
  Classes, SysUtils, HPDFHeadlessDocument;

const
  // 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 escaneo lee banderas a nivel de campo, que es donde las ponen los productores; el renderer también honra un /Ff en un widget separado, así que trate la lista como una ayuda de planificación y al diagnóstico de render como la palabra final. La comprobación del campo en blanco importa porque un campo de texto sin /V ni /AP no tiene nada que sintetizar: el render 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 archivo llegue al renderer

¿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. ISO 32000-1 §7.9.5 permite que un rectángulo nombre dos esquinas diagonales opuestas cualesquiera, y su nota le dice al software de procesamiento que se prepare para normalizarlas. Un renderer 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 refleja su apariencia. HotPDF intercambia las coordenadas al orden inferior-izquierda, 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 se lanza solo para una anotación que tiene un 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 un punto bajo su /Matrix. Cuando aparece, vuelque el /Rect del widget junto con el /BBox y el /Matrix del flujo: uno de los tres tiene una 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. 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 colapsa a un solo punto. Un rectángulo que es apenas una línea todavía debe una apariencia bajo PDF 2.0; el renderer también lo salta, simplemente porque no hay área que pintar

Cuando no se encuentra el tipo de campo

"Widget field requires a name field type" significa que el renderer subió desde el widget todo lo que llegaron los enlaces /Parent y no encontró ningún /FT. El renderer parte del arreglo /Annots de la página, así que el enlace /Parent hacia arriba es su única ruta al campo. La carga camina al revés, hacia abajo desde el arreglo /Fields del AcroForm por los /Kids, razón por la que LoadFromFile puede tener éxito en un archivo cuya página después falla al renderizar. Una causa típica es un widget listado en los /Kids de un campo que no apunta de vuelta. ISO 32000-1, Tabla 188, da cierta cobertura para eso: exige /Parent en un widget solo cuando el widget es uno de varios hijos y dice "absent otherwise", así que un writer que la lee al pie de la letra suelta el enlace para un campo con un solo widget separado. ISO 32000-2 relajó la misma entrada a "optional otherwise". Nosotros escribiríamos /Parent en cada widget que no esté fusionado con su campo de todos modos. ISO 32000-2 lo permite, y es la única manera en que un lector guiado por la página, el nuestro o el de cualquier otro, puede alcanzar el campo. Si no puede arreglar al productor, fusionar un widget solitario en el diccionario de su campo da el mismo resultado

Un widget que apunta a su padre y encuentra un /DA en algún punto del camino hacia arriba renderiza correctamente sin ningún /AP en el renderer headless del HotPDF Delphi Component, lo que hace del árbol de campos, más que de los flujos de apariencia, lo primero que hay que dejar bien en un pipeline de formularios Linux