Na lista de campos de quase qualquer visualizador de PDF, o campo de rua num formulário de reclamação em alemão aparece como Straße. Os bytes por trás da entrada /T dele contam outra história: <FEFF005300740072006100DF0065>, uma string hexadecimal UTF-16BE com uma byte order mark na frente. Código que casa nomes de campo contra esses bytes brutos reporta o campo como ausente, mesmo que um usuário possa clicar direto nele. O mesmo formulário pode guardar o estado on de um checkbox como 2 em vez de Yes e a lista de países dele como pares [export display], o que lhe dá três maneiras separadas de o valor que você escreve e o valor que um viewer pinta divergirem
As três variações vêm do mesmo lugar: um formulário construído anos atrás na ferramenta de autoria que o cliente tinha à mão, um que a sua aplicação Delphi agora precisa preencher a partir de um banco de dados e devolver como um arquivo que renderiza igual em todo leitor. Você não pode re-autorar o formulário, então o código de preenchimento precisa seguir as regras sob as quais o formulário foi escrito. A ISO 32000-1 tem uma regra para cada um dos três casos: §7.9.2.2 para text strings, §12.7.4.2.3 para estados de aparência de botões, Tabela 231 na §12.7.4.4 para seleções de choice. Criar campos que ainda não existem é outro trabalho, coberto em adicionar campos AcroForm a um PDF carregado em Delphi
Achando o campo: o HotPDF decodifica /T antes de casar nomes
THotPDF.SetFormFieldValue tem dois overloads, um que recebe um índice de campo zero-based e um que recebe um nome totalmente qualificado, e ambos resolvem contra uma tabela de nomes que o componente constrói da árvore AcroForm no primeiro uso. Enquanto a tabela é construída, todo nome parcial passa por um decodificador. Uma string /T hexadecimal é transformada de volta em bytes; se esses bytes começam com FE FF e têm comprimento par, o resto é lido como UTF-16BE e re-codificado como UTF-8. Cada nome parcial decodificado é então unido ao parent dele com um ponto, exatamente como a §12.7.3.1 descreve, então um campo kid chamado City sob um parent chamado Address é registrado como Address.City. Só a chave de busca é decodificada. O objeto /T no arquivo mantém a grafia hex dele, e salvar o documento não toca na identidade de um campo que você apenas preencheu
As chaves são postas em minúsculas antes de entrar na tabela, então 'address.city' encontra Address.City. Isso vai além do padrão, que trata nomes de campo como case-sensitive, e significa que dois campos diferindo só na caixa colidiriam na busca. Para código que lê nomes de campo de um arquivo de configuração digitado à mão, uma maiúscula perdida é uma falha bem mais comum do que dois campos num mesmo formulário que diferem só pela caixa
Os nomes decodificados são bytes UTF-8 carregados num AnsiString, e é aí que as conversões de string do próprio Delphi atrapalham. Um literal como 'Applicant.Straße' passado a um parâmetro AnsiString é convertido pela página de código ANSI do sistema, então ß chega como o byte único DF em vez do par UTF-8 C3 9F, e a busca falha. Nomes ASCII não são afetados. Para qualquer outra coisa, enumere os campos uma vez, decodifique cada nome com UTF8ToString e escreva pelo overload de índice:
// FindFieldIndex é um helper que você mesmo acrescenta: ele compara os
// nomes UTF-8 decodificados com uma string Unicode, então nenhuma página
// de código ANSI fica entre o seu literal e a busca
function FindFieldIndex(Pdf: THotPDF; const Name: string): Integer;
var
I: Integer;
begin
for I := 0 to Pdf.GetFormFieldCount - 1 do
if SameText(UTF8ToString(Pdf.GetFormFieldName(I)), Name) then
Exit(I);
Result := -1;
end;
var
Pdf: THotPDF;
Street: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Nomes ASCII podem ir direto ao overload por nome
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Street := FindFieldIndex(Pdf, 'Applicant.Straße');
// O valor também é um AnsiString, então um 'ß' literal dependeria da
// página de código do sistema; hex FEFF (Hauptstraße 12) evita isso
if Street >= 0 then
Pdf.SetFormFieldValue(Street,
'FEFF00480061007500700074007300740072006100DF0065002000310032');
// O mesmo hex UTF-16BE com prefixo FEFF funciona com o overload por nome
// (este é München); ele é escrito como uma string hex
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
O que o SetFormFieldValue escreve, passo a passo
Ambos os overloads rodam a mesma sequência depois que o campo é achado: escrever /V, reconstruir o índice de seleção do choice, marcar o campo como alterado, trocar estados de aparência de botão, regenerar a aparência de texto ou choice e registrar o campo num conjunto de sujos. Esse último passo alimenta o RecalculateLoadedFormFieldsIncremental sem parâmetros, que re-executa apenas os scripts de cálculo que leem um campo alterado, direta ou por meio de uma cadeia como item de linha, subtotal, total
O tipo de objeto do novo /V depende do campo. Para um checkbox ou botão de rádio, SetFormFieldValue sempre escreve um objeto name, porque a §12.7.4.2.3 guarda o estado selecionado como um name e nomes PDF são bytes puros sem encoding de text string. Todo outro campo recebe um objeto string, e o valor que você passa é inspecionado primeiro. Uma string que começa com FEFF, tem comprimento par e contém só dígitos hex é tomada como a forma UTF-16BE da §7.9.2.2 e serializada como <FEFF...>. É disso que a linha do City acima depende. Qualquer outra coisa é guardada como string literal com exatamente os bytes que você forneceu, então texto latino simples entra como texto simples
Por que um checkbox mantém o tick antigo depois de o valor mudar?
Para um campo botão, o valor não decide o que é desenhado. Um widget checkbox carrega uma entrada /AS que nomeia qual stream em /AP /N está sendo exibido no momento, e os viewers pintam a partir do /AS, não do /V. Mude o /V para Yes enquanto o /AS ainda diz Off e o arquivo se contradiz; um flattener vai assar a caixa desmarcada na página enquanto os dados do formulário afirmam que ela está marcada. SetFormFieldValue portanto visita o campo botão e toda entrada no array /Kids dele, lê o nome de on-state do /AP /N de cada widget e define o /AS com esse nome quando casa com o valor do campo, ou com Off quando não casa
Formulários reais contornam essa regra de dois jeitos. A aparência off se chama Off no padrão, mas ferramentas de autoria frequentemente deixam o stream dela de fora por completo e deixam o viewer não desenhar nada, então um dicionário de aparência normal com uma entrada só é perfeitamente legal. O nome de on-state é o que o autor escolheu: Yes, On, 2, uma palavra no idioma do próprio formulário. Então o on state é tomado como a primeira chave no /AP /N que não é Off, e a comparação com o seu valor ignora a caixa. Botões de rádio acrescentam o arranjo da §12.7.4.2.4: o valor selecionado mora no /V do campo parent, enquanto cada widget kid normalmente não tem /V próprio. Cada kid é comparado contra o valor herdado pela cadeia /Parent dele
// Checkbox: o valor precisa ser igual à chave de on-state no /AP /N
// ('Yes' aqui; o designer do formulário pode ter escolhido '2' ou 'On')
Pdf.SetFormFieldValue('Consent', 'Yes');
// Grupo de rádio: /V vai no parent como name; todo widget kid
// recebe /AS definido com o próprio nome de on-state dele ou Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Limpar um checkbox: um valor que não casa com nenhum on-state resulta em /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
O lado ruim é que um erro de digitação não produz exceção. 'yes ' com espaço no final não casa com nada, então a caixa termina em Off e o arquivo continua perfeitamente válido. Se os seus valores vêm de uma coluna de banco de dados, apare-os antes da chamada
Campos choice: manter /I em passo com /V
Um combo box ou list box registra a seleção dele em dois lugares. Ao lado do /V, a Tabela 231 define /I como um array de índices zero-based dentro do /Opt, e um viewer que achar o /I apontando para a opção 0 enquanto o /V nomeia a opção 3 pode destacar a linha errada. SetFormFieldValue reconstrói o /I em cada chamada a um campo choice. Ele apaga a entrada /I própria do campo antes de qualquer outra coisa e nunca edita o array velho no lugar: um array indireto pode ser compartilhado com outro campo, e mutá-lo mudaria a seleção desse campo também. Ele então resolve o /Opt pela cadeia /Parent, já que opções podem ser herdadas, e percorre as entradas. Uma opção string pura é comparada diretamente, um par [export display] no elemento de exportação, e um par com menos de dois elementos é pulado
A comparação roda sobre texto decodificado, então uma opção armazenada em PDFDocEncoding casa com um valor escrito como hex UTF-16BE e vice-versa. Um valor escalar produz no máximo um índice; qualquer multi-seleção que o campo carregava antes é substituída. Quando nada casa, nenhum /I é escrito. Para um combo box editável esse é o resultado correto, porque a §12.7.4.4 deixa o usuário digitar um valor fora da lista e esse valor não tem índice para apontar
A mesma regra está por trás da reclamação mais comum sobre combo boxes: a chamada tem sucesso, o /V guarda o texto novo, e o viewer não mostra nada selecionado. Normalmente quem chamou passou o rótulo de exibição (Canada) para uma lista construída de pares [CA Canada]. O componente não pode tratar isso como erro sem quebrar combos editáveis, então a checagem mora no seu código. GetFormFieldOptionValue devolve o elemento de exportação de cada opção, o que deixa a guarda curta:
// SetChoiceByExport é uma guarda sua, não faz parte do componente:
// ela recusa valores que não casam com elemento de exportação algum no /Opt
procedure SetChoiceByExport(Pdf: THotPDF; const FieldName,
ExportValue: AnsiString);
var
I: Integer;
begin
for I := 0 to Pdf.GetFormFieldOptionCount(FieldName) - 1 do
if Pdf.GetFormFieldOptionValue(FieldName, I) = ExportValue then
begin
// /Opt é [[US United States] [CA Canada] [MX Mexico]]:
// 'CA' casa com o elemento de exportação e /I vira [1]
Pdf.SetFormFieldValue(FieldName, ExportValue);
Exit;
end;
raise Exception.CreateFmt('%s is not an export value of %s',
[string(ExportValue), string(FieldName)]);
end;
// Combo editável com texto livre: escreva direto. /V é definido,
// /I é removido, e nenhum índice é inventado
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
O novo valor aparece sem /NeedAppearances?
Sim, para campos de texto e choice. Um viewer desenha o stream /AP armazenado e não olha o /V, então um valor novo com uma aparência velha deixa o texto velho na tela e na impressão a menos que o dicionário AcroForm carregue /NeedAppearances true (§12.7.3.3) e o viewer escolha honrá-lo. Flatteners e geradores de thumbnail frequentemente ignoram essa flag. SetFormFieldValue regenera o appearance stream para campos de texto e choice como parte da chamada, usando o mesmo builder do EnsureLoadedFieldAppearanceStream público. O stream é construído a partir da string /DA herdada, do alinhamento /Q, do /MaxLen para campos comb e do próprio valor. A fonte nomeada no /DA é resolvida pelos recursos /DR do AcroForm, então uma fonte Type0 mantém os próprios glifos em vez de cair para Helvetica. Botões mantêm as aparências de estado que o designer deles desenhou; para eles só o /AS se move
SetFormFieldValue é uma procedure e não conta se algo foi pintado. Um widget sem um /Rect utilizável não recebe stream, e formulários quebrados com widgets órfãos ou sem retângulo existem. Quando o resultado renderizado importa, chame o EnsureLoadedFieldAppearanceStream você mesmo depois de definir o valor. Ele devolve True quando pelo menos um widget recebeu um stream. GetFormField lhe entrega um THPDFLoadedFormField cuja propriedade Index é o que a chamada espera; o objeto é seu e você precisa liberá-lo
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// False significa que nenhum widget desse campo tinha retângulo para
// pintar; o valor é armazenado, mas nada vai exibi-lo
if not Pdf.EnsureLoadedFieldAppearanceStream(Field.Index) then
raise Exception.Create('No widget rectangle to paint into');
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
end;
Checar que o /V mudou prova muito pouco sobre o que um usuário verá. Um teste que significa algo renderiza a página antes e depois e confirma que pixels dentro do retângulo do widget mudaram enquanto pixels fora dele não. Mantemos essa checagem porque os bytes de aparência sozinhos já nos enganaram: um stream guardava todo byte certo enquanto um viewer honrando /NeedAppearances true reconstruía a exibição a partir do /V e não mostrava nada daquilo
Relendo valores, e o que costuma vir a seguir
GetFormFieldValue devolve o /V herdado do campo. Um valor guardado como hex com prefixo FEFF volta decodificado para UTF-8, então o campo City acima é relido como os bytes UTF-8 de München e UTF8ToString o transforma numa string Delphi. Uma string literal volta com os bytes com que foi guardada. Para checagens de round trip, compare texto decodificado dos dois lados em vez da grafia que você passou
Mover dados de campo em massa entre o formulário e um sistema externo, em vez de uma chamada por campo, é o assunto de importação e exportação de XFDF em Delphi. Trancar um formulário pronto para ninguém editá-lo é achatar campos AcroForm e XFA em Delphi, que transforma exatamente os estados /AS e os appearance streams descritos aqui em conteúdo estático de página. O que estiver inconsistente nesse ponto fica inconsistente para sempre, então preencha, cheque e depois achate
SetFormFieldValue, EnsureLoadedFieldAppearanceStream e a API de recálculo incremental fazem parte do HotPDF Delphi Component, ao lado das chamadas que renomeiam, removem e achatam campos num documento que você carregou a partir de Delphi ou C++Builder