Artigo Técnico

Como Renderizar Campos de Formulário PDF sem /AP no Linux

Um formulário PDF com valores de campo mas sem streams de aparência pode sair de um job de renderização Linux como caixas em branco, fonte ou alinhamento errados, ou um EHPDFHeadlessRenderError com a mensagem "Widget field requires a name field type". O renderer headless do HotPDF desenha aparências temporárias para widgets de texto, botão e escolha, herdando /FT, /V, /DA e /Q pela árvore de campos

Onde um widget guarda as suas configurações de campo

Quase todo o problema começa na divisão entre um campo e os seus widgets. O dicionário do campo guarda a parte lógica de um controle de formulário: o tipo, o valor, as flags e a aparência padrão dele. A anotação widget guarda o que uma página precisa: /Rect, borda, um ponteiro para a página dela. Um campo com um widget geralmente vem mesclado em um único dicionário. Quando o mesmo valor precisa aparecer em várias páginas, os widgets viram filhos do campo e muitas vezes não carregam nada além de geometria e uma referência /Parent. A ISO 32000-1 §12.7.3.1 torna /FT, /Ff, /V e /DV herdáveis por essa cadeia (Tabela 220), e o §12.7.3.3 acrescenta só dois atributos de texto variável ao conjunto herdável da Tabela 222: o /DA, que é obrigatório mas pode vir de um ancestral, e o /Q. O dicionário de formulário interativo (Tabela 218) fornece padrões para o documento inteiro de /DA e /Q, mais o /DR, o dicionário de recursos padrão no qual os nomes de fonte do /DA são procurados. O /DR não está na Tabela 222; ele pertence ao formulário como um todo. Um renderer que lê só o dicionário do widget não encontra tipo, valor ou /DA nenhum no arquivo abaixo e recua para a fonte padrão dele

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 e padrões de aparência moram aqui
<< /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 na página 1: só geometria, cantos listados com o topo-direito primeiro
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [300 720 160 700] /F 4 /P 3 0 R >>
endobj
12 0 obj  % widget na página 2: só geometria
<< /Type /Annot /Subtype /Widget /Parent 10 0 R
   /Rect [160 60 300 80] /F 4 /P 4 0 R >>
endobj

O HotPDF procura cada entrada de campo no widget e, não achando, sobe o /Parent um dicionário por vez; o valor mais próximo vence, e só o /DA e o /Q recuam para o dicionário AcroForm quando a cadeia se esgota. Esperado para o arquivo acima: os dois widgets mostram INV-2026-0417 alinhado à direita em Courier de 10 pontos, azul escuro, e o /Helv 0 Tf de todo o documento nunca entra em cena porque o objeto 10 tem o /DA próprio dele. Quando um campo sai em Helvetica preta de 12 pontos, o suspeito óbvio é uma fonte Courier faltando na máquina Linux. O tamanho denuncia. É o que o HotPDF desenha quando nenhum /DA aparece no widget, em nenhum ancestral ou no dicionário AcroForm. Então despeje o widget, siga o /Parent dele à mão e descubra onde o produtor pôs o /DA, por exemplo em um dicionário de campo ao qual o widget nunca aponta de volta

Grafo de objetos AcroForm para renderização headless do HotPDF em Delphi: widgets só de geometria sobem o /Parent até o campo que guarda /FT, /V, /DA e /Q
o valor mais próximo pela cadeia /Parent vence, e só o /DA e o /Q recuam para o dicionário AcroForm quando a cadeia se esgota

Nomes de fonte do /DA resolvem contra um /DR no widget ou em um campo pai, o /DR do AcroForm e os recursos de fonte da página, nessa ordem. Um /DR de nível de campo está fora da Tabela 222, mas não é ambíguo quando presente, então o HotPDF o aceita. A subida tem limite: um laço na árvore para com "Widget field inheritance contains a cycle", um /Parent que não é dicionário com "Widget Parent requires a dictionary", e uma cadeia mais profunda que o MaxDepth nas opções de renderização (32 por padrão) com "Widget field inheritance depth budget exceeded". O componente VCL aplica a mesma herança quando lê valores AcroForm herdados e reseta campos

Como o renderer headless decide o que desenhar?

O HPDFRenderHeadlessPagePNG e a sua variante de arquivo rodam o fallback só para um widget visível cujo /AP está ausente ou é null; a ISO 32000-1 §7.3.7 diz que um valor de dicionário null é o mesmo que uma entrada ausente, e o renderer trata /AP null desse jeito. Uma aparência normal presente sempre vence, diga o que disser o /NeedAppearances, então uma aparência obsoleta de um preenchimento anterior continua aparecendo até algo regenerá-la. O documento carregado nunca é modificado. O que é desenhado depende do tipo e das flags de campo resolvidos:

CampoAparência sintetizada
Texto de linha única, /FT /TxA string do /V, disposta com o /DA e o /Q resolvidos dentro do /Rect
Checkbox, /FT /BtnBorda retangular com raios de canto do /Border, preenchimento /MK /BG, e um tick vetorial traçado quando ligado
Botão de rádio, Btn com bit 16Borda circular com um ponto preenchido quando ligado
Pushbutton, Btn com bit 17Borda retangular com a legenda do /MK /CA centralizada
Combo box, /FT /Ch com bit 18O texto de exibição da entrada /Opt selecionada como legenda centralizada
List box, Ch com bit 18 zeradoUma linha por opção a partir do /TI, com uma faixa de destaque atrás de cada linha selecionada

O check mark e o ponto do rádio são paths vetoriais em vez de glifos ZapfDingbats, como muitos geradores desenham, porque um contêiner Linux enxuto pode não ter ZapfDingbats instalado. As faces de pushbutton e de combo box batem com o que o gerador de aparências VCL do HotPDF desenha para o mesmo campo, então um formulário fica com a mesma cara independente do lado que produziu a imagem. Uma legenda tira a fonte, o tamanho e a cor do /DA; como o layout de legenda não se ajusta sozinho, um tamanho /DA de 0 vira 70 por cento da altura da caixa, com teto de 12 pontos. O estado de checkbox e rádio vem do /AS do próprio widget, recuando para o /V herdado, e qualquer estado diferente de Off desenha a marca. Dê a cada filho de rádio o /AS próprio dele. O /V do pai nomeia o estado selecionado para o grupo inteiro e não distingue um filho do outro

Fluxo de decisão no renderer headless do HotPDF para Delphi: um /AP presente sempre vence, senão o tipo de campo resolvido escolhe uma face sintetizada
aparências temporárias são desenhadas só quando o /AP está ausente ou é null, e o documento carregado em si nunca é modificado
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 a impressão de um visualizador: widgets sem a flag Print são pulados
    try
      // PageIndex é zero-based
      Info := HPDFRenderHeadlessPagePNGFile(Doc.GetRenderAccess(PageIndex),
        PngName, Options);
      Diagnostic := Format('%d appearances drawn', [Info.Appearances]);
      Result := True;
    except
      on E: EHPDFHeadlessRenderError do
        // por exemplo 'Widget field requires a name field type'; para um formulário preenchido, não
        // repita com RenderAnnotations := False, o que descarta todo widget e o seu valor
        Diagnostic := E.Message;
    end;
  finally
    Doc.Free;
  end;
end;

O fallback de campo de texto desenha campos de linha única que guardam um valor string. Para qualquer layout de texto mais rico, gere a aparência na hora do preenchimento do lado Windows, onde o EnsureLoadedFieldAppearanceStream constrói o stream /AP que falta antes de o arquivo ser achatado ou enviado; o job Linux então recebe streams de aparência de verdade, e o renderer headless nomeia qualquer campo que encontre que ainda precise de um. Julgue a saída Linux pelo renderer Linux, porém. O RenderLoadedPageToBitmap desenha pelo backend GDI do Windows, que nunca roda esse fallback, então uma página que parece certa ali não diz nada sobre o que o dispositivo Cairo vai produzir do mesmo arquivo

Os bits de flag de campo são numerados a partir de um

A ISO 32000-1 §12.7.3.1 afirma que as posições de bit na palavra /Ff vão de 1 na ponta de baixa ordem até 32, e as Tabelas 226, 228 e 230 todas usam essa numeração. Em Delphi, o bit N é portanto 1 shl (N - 1). Desloque pelo número do bit em si e todo teste sobe uma posição: 1 shl 16 escrito para "radio, bit 16" na verdade testa a flag de pushbutton, e 1 shl 18 escrito para "combo, bit 18" testa Edit. As duas máscaras erradas são flags reais. O código compila, roda, e classifica os campos no tipo errado

Flags de campo PDF numeram bits a partir de um em Delphi: a régua mostra por que rádio é 1 shl 15 e 1 shl 16 alcança pushbutton
o bit N da palavra /Ff recebe a máscara Delphi 1 shl N menos um, e toda máscara deslocada de um é uma flag real do bit vizinho
FlagTabelaBitMá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ão)161 shl 15 = 32768
Pushbutton226 (botão)171 shl 16 = 65536
Combo230 (escolha)181 shl 17 = 131072
uses
  Classes, SysUtils, HPDFHeadlessDocument;

const
  // A ISO 32000-1 numera os bits de Ff a partir de 1 (baixa ordem), então o 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 chegam já herdados dos ancestrais
      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;

A varredura lê as flags de nível de campo, que é onde os produtores as põem; o renderer também honra um /Ff em um widget separado, então trate a lista como um auxílio de planejamento e o diagnóstico de renderização como a palavra final. A checagem de vazio importa porque um campo de texto sem /V e sem /AP não tem nada para sintetizar: a renderização para com "A text-field widget without a value has no appearance to synthesize", e a cura é um valor ou uma aparência antes de o arquivo chegar ao renderer

O que significa "Degenerate appearance geometry"?

Significa que o retângulo ou a caixa de aparência não tem área, e um /Rect invertido não é a causa. A ISO 32000-1 §7.9.5 deixa um retângulo nomear quaisquer dois cantos diagonalmente opostos, e a nota dela manda o software de processamento estar preparado para normalizá-los. Um renderer ingênuo que calcula a largura como x2 - x1 obtém um número negativo de [300 720 160 700] e ou rejeita a anotação ou espelha a aparência dela. O HotPDF troca as coordenadas para a ordem inferior-esquerda e superior-direita antes de qualquer checagem de tamanho, tanto para faces sintetizadas quanto para streams /AP existentes, então o widget 11 acima renderiza como se os cantos dele fossem convencionais. O erro é levantado só para uma anotação que tem um stream de aparência quando, depois da normalização, o /Rect tem largura ou altura zero, ou o /BBox do stream colapsa para uma linha ou um ponto sob o /Matrix dele. Quando aparecer, despeje o /Rect do widget junto com o /BBox e o /Matrix do stream: um dos três tem extensão zero

Um widget sem /AP e com um /Rect de área zero segue outro caminho: o fallback o pula e continua com a página. A ISO 32000-2 §12.5.2 isenta da exigência de aparência só as anotações Popup, Projection e Link e um retângulo que colapsa para um único ponto. Um retângulo que é apenas uma linha ainda deve uma aparência sob PDF 2.0; o renderer pula esse também, simplesmente porque não há área para pintar

Quando o tipo de campo não pode ser encontrado

"Widget field requires a name field type" significa que o renderer subiu do widget até onde os links /Parent chegaram e não encontrou nenhum /FT. O renderer começa pelo array /Annots da página, então o link /Parent para cima é a única rota dele até o campo. O carregamento anda para o outro lado, descendo do array /Fields do AcroForm pelos /Kids, e é por isso que o LoadFromFile pode ter sucesso em um arquivo cuja página depois falha ao renderizar. Uma causa típica é um widget listado nos /Kids de um campo que não aponta de volta. A ISO 32000-1 Tabela 188 dá alguma cobertura para isso: ela exige /Parent em um widget só quando o widget é um de vários filhos e diz "absent otherwise", então um writer que lê ao pé da letra derruba o link para um campo com um único widget separado. A ISO 32000-2 relaxou a mesma entrada para "optional otherwise". Nós escreveríamos /Parent em todo widget que não vem mesclado com o seu campo de qualquer jeito. A ISO 32000-2 permite, e é a única maneira pela qual um leitor orientado à página, o nosso ou o de qualquer um, alcança o campo. Se você não pode consertar o produtor, mesclar um widget solitário no dicionário do campo dele dá o mesmo resultado

Um widget que aponta para o pai e acha um /DA em algum ponto da subida renderiza certo sem nenhum /AP no renderer headless do componente HotPDF para Delphi, o que faz da árvore de campos, mais do que dos streams de aparência, a primeira coisa a acertar em um pipeline de formulários Linux