O HotPDF Delphi Component preenche um campo AcroForm existente num PDF carregado por meio do THotPDF.SetFormFieldValue, endereçado por índice de campo zero-based ou por nome de campo totalmente qualificado. Escrever a nova entrada /V é a parte fácil; o que torna a chamada confiável em formulários do mundo real é que o mesmo método também mantém consistente três pedaços de estado que ficam invisíveis até darem errado: a identidade decodificada do campo, para que um nome não ASCII possa ser encontrado de algum jeito, o estado de aparência /AS em widgets de checkbox e radio, e o array /I de índices de seleção em campos choice. O appearance stream visível é um passo à parte e explícito, via EnsureLoadedFieldAppearanceStream
O cenário é o mais banal: um cliente te manda o formulário dele, uma declaração de imposto de renda, uma apólice de seguro, um pedido de compra que alguém montou no Acrobat anos atrás, e sua aplicação Delphi precisa preenchê-lo a partir de um banco de dados e devolver um arquivo que abra corretamente em todo lugar. Você não tem controle nenhum sobre como o formulário foi criado. Nomes de campo podem estar em UTF-16, valores de export de checkbox podem ser 2 em vez de Yes, e combo boxes podem usar pares de opção [export display]. Cada um desses detalhes tem uma regra na ISO 32000-1, e cada regra é algo que o SetFormFieldValue agora resolve por você. Este artigo é sobre o que ele faz, por quê, e onde ele para. Para o problema irmão de criar campos que ainda não existem, veja adicionar campos AcroForm a um PDF carregado no Delphi
Por que o SetFormFieldValue não encontra um campo com nome não ASCII?
Antes da v2.752.1 a resposta era codificação: o campo vivia no arquivo sob um nome hexadecimal UTF-16BE, e o cache de nomes guardava a grafia hex em vez do texto. A ISO 32000-1 §12.7.3.1 define o nome parcial de campo /T como uma text string, e a §7.9.2.2 diz que uma text string pode ser UTF-16BE com um byte order mark FE FF na frente. Ferramentas de autoria rotineiramente serializam esses nomes como strings hex conforme a §7.3.4.3, então um campo chamado Straße chega como <FEFF005300740072006100DF0065>. Dentro do HotPDF, THPDFStringObject.Value guarda o texto hexadecimal cru sempre que IsHexadecimal está setado, o que é exatamente o que você quer para um round trip sem perdas do dicionário original e exatamente o que você não quer como chave de busca. O HPDFLoadedFormTextName separa as duas preocupações. Quando o cache de relacionamentos é construído, todo valor /T passa por ele: se o objeto string é hexadecimal, HPDFHexToBytes restaura a sequência de bytes; se os bytes começam com FE FF e têm comprimento par, o payload é decodificado como UTF-16BE e re-codificado como UTF-8; o resultado é então unido ao nome do pai com um ponto, para formar o nome totalmente qualificado que a §12.7.3.1 descreve, então um filho chamado City sob um pai chamado Address fica registrado como Address.City. A chave do cache é normalizada para minúsculas, o que faz SetFormFieldValue('address.city', ...) funcionar também; isso é uma conveniência além do padrão, já que a especificação trata nomes como case-sensitive. Crucialmente, só a chave do cache muda. O objeto /T no dicionário do campo mantém sua codificação hexadecimal, então salvar o documento não reescreve a identidade de um campo que você apenas preencheu
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Nomes qualificados são decodificados de strings /T UTF-16BE e
// unidos com pontos, então nomes aninhados e não ASCII resolvem
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Pdf.SetFormFieldValue('Applicant.Straße', 'Hauptstraße 12');
// Valores que não são Latin-1 viajam como hex UTF-16BE com prefixo FEFF
// e são escritos como string hexadecimal de PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
O que o SetFormFieldValue escreve de fato?
As duas overloads rodam os mesmos cinco passos: localizar o dicionário do campo, escrever /V via HPDFSetDictFormValue, reconciliar os índices de seleção de choice, marcar o dicionário como sujo, reconciliar os estados de aparência dos botões e, por fim, registrar o índice do campo via NoteLoadedFormFieldDirty. Esse último passo importa se o formulário carrega scripts de cálculo, porque o conjunto de sujos é o que a overload sem parâmetros de RecalculateLoadedFormFieldsIncremental consome para rerodar apenas os cálculos que leem transitivamente um campo alterado. O próprio HPDFSetDictFormValue toma cuidado com o tipo de objeto que substitui. Se o /V existente é um name object, que é o que campos de checkbox e radio usam para seu valor de export, o novo valor é escrito como name, nunca como string, porque nomes PDF são ASCII por construção. Caso contrário, ele escreve um objeto string e inspeciona o valor que você passou: uma string que começa com FEFF, tem comprimento par e consiste apenas de dígitos hex é tratada como a forma de fio UTF-16BE da §7.9.2.2 e guardada com IsHexadecimal setado, então ela serializa como <FEFF...> em vez de como um literal (FEFF...). É nesse mecanismo que a linha do City acima se apoia; qualquer outra string é guardada como string literal com os bytes que você deu, então para texto latino comum você passa texto comum
Por que um checkbox mantém o tick antigo depois que o valor muda?
Porque, num campo de botão, o valor sozinho não decide o que é desenhado. A ISO 32000-1 §12.7.4.2.3 especifica que um widget de checkbox carrega um estado de aparência /AS nomeando qual stream em /AP /N está sendo mostrado naquele momento, e os viewers pintam a partir de /AS, não de /V. Se você muda /V para Yes mas deixa /AS em Off, o arquivo fica internamente contraditório, e o flatten vai feliz assar a aparência desmarcada obsoleta dentro da página enquanto os dados do formulário dizem marcado. O ReconcileLoadedButtonAppearanceStates existe para fechar essa lacuna: para um campo cujo /FT é Btn, ele visita o próprio dicionário do campo e toda entrada do seu array /Kids, lê o nome do estado on em /AP /N e reescreve /AS para esse nome quando ele casa com o valor do campo, ou para Off quando não casa
Dois detalhes de formulários reais moldaram a correção da v2.752.3. Primeiro, um dicionário de aparência normal pode conter apenas o estado on; a §12.7.4.2.3 chama a aparência off de Off, mas as ferramentas de autoria frequentemente omitem seu stream e deixam o viewer não desenhar nada. O código anterior desistia quando o dicionário tinha menos de duas entradas, então aqueles checkboxes de estado único mantinham o tick antigo em silêncio. A checagem agora é simplesmente que o dicionário não está vazio, e o nome do estado on é tomado como a primeira chave que não é Off. Segundo, o nome do estado on é o que o autor escolheu. Formulários reais usam 2, Yes, On ou uma palavra localizada, então a comparação é contra a chave de verdade, sem diferenciar maiúsculas, nunca contra um Yes hard-coded. Radio buttons trazem mais uma complicação, descrita na §12.7.4.2.4: a seleção vive em /V no campo pai, enquanto os filhos individuais são donos dos widgets e tipicamente não têm /V próprio. O helper aninhado InheritedButtonValue portanto sobe a cadeia /Parent, até 64 níveis, até encontrar um valor não vazio, para que cada filho seja comparado contra o valor do grupo a que pertence. Definir o pai com o valor de export de um filho liga exatamente aquele filho e desliga todos os irmãos
// Checkbox: o valor de export precisa casar com a chave de estado on em /AP /N
// (muitas vezes 'Yes', mas formulários reais usam '2', 'On' ou qualquer outra coisa)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Grupo de radio: o /V é escrito no pai; todo widget filho recebe
// /AS com seu próprio nome de export ou Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Limpar um checkbox: qualquer valor que não case com nenhum estado on deixa /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Campos choice: manter /I em sincronia com /V
Num combo box ou list box, /V não é o único lugar em que uma seleção fica registrada. A Tabela 231 da §12.7.4.4 define /I como um array de índices zero-based em /Opt que identifica os itens selecionados, e um viewer que encontre /I apontando para a opção 0 enquanto /V nomeia a opção 3 pode destacar a linha errada. Desde a v2.754.1, o HPDFReconcileChoiceSelection roda dentro de toda chamada a SetFormFieldValue e, quando o /FT herdado é Ch, reconstrói /I a partir do novo valor. A ordem das operações é deliberada. A entrada /I local é apagada primeiro, sem tocar no seu conteúdo: se o array antigo era um objeto indireto compartilhado com outro campo, mutá-lo no lugar corromperia a seleção do outro campo, então a rotina descarta a referência e cria um array direto novo. Ela então resolve /Opt pela cadeia /Parent, já que opções de choice podem ser herdadas, e varre as entradas. Uma opção string pura é comparada diretamente; um par [export display] é comparado pelo seu elemento de export, e um par com menos de dois elementos é pulado. Os dois lados passam por HPDFLoadedFormTextName, então uma opção hex UTF-16 casa com um valor hex UTF-16 sem que você precise escrevê-los de forma idêntica. No primeiro match um /I de um elemento é escrito e a varredura para; um valor escalar sempre substitui qualquer multi-seleção anterior, independentemente da flag MultiSelect
Quando nada casa, nenhum /I é escrito. Esse é o resultado correto para um combo box editável, em que a §12.7.4.4 permite que o usuário digite um valor fora da lista de opções; um valor desses não tem índice, e um índice obsoleto seria pior que nenhum. Também é o que você obtém se passar um rótulo de display em vez de um valor de export para uma lista de opções em pares, então quando um combo box se recusa a mostrar sua seleção, confira qual metade do par você forneceu
// /Opt é [[US United States] [CA Canada] [MX Mexico]]:
// casa pelo valor de export, e /I vira [1]
Pdf.SetFormFieldValue('Country', 'CA');
// Combo editável com um valor fora de /Opt: /V é escrito,
// /I é removido, e nenhum índice é inventado
Pdf.SetFormFieldValue('Title', 'Principal Engineer');
Valor e aparência são duas operações separadas
O SetFormFieldValue nunca toca no appearance stream de um campo de texto ou de choice. Depois da chamada, /V guarda o texto novo enquanto /AP /N ainda pinta o antigo, e qual dos dois o viewer mostra depende de o dicionário AcroForm carregar /NeedAppearances true conforme a §12.7.3.3 e de o viewer honrar isso. Se você precisa que o arquivo renderize o valor novo em todo leitor, incluindo flatteners e geradores de thumbnail que ignoram a flag, chame EnsureLoadedFieldAppearanceStream com o índice do campo. Ele monta um Form XObject a partir da string /DA herdada, do quadding /Q, do layout comb de /MaxLen e do valor, resolve a fonte nomeada pelos recursos /DR do AcroForm para que uma fonte Type0 mantenha sua própria descendant font em vez de degradar para Helvetica, e devolve True quando ao menos um widget recebeu um stream. A overload por nome de SetFormFieldValue não te devolve índice, então obtenha um via GetFormField, que retorna um THPDFLoadedFormField que é seu e que você precisa liberar. A suíte de regressão da mudança da v2.752.1 é explícita quanto a essa divisão: ela define um valor, chama EnsureLoadedFieldAppearanceStream, então renderiza a página e confere que os pixels dentro do retângulo do widget mudaram enquanto os de fora não. Verificar que o /V mudou não prova nada sobre o que o usuário vai ver
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Pinta o valor novo em /AP para que viewers que ignoram
// /NeedAppearances ainda o mostrem
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;
Limites que vale conhecer antes de construir em cima disso
O ReconcileLoadedButtonAppearanceStates testa o /FT local do dicionário que você endereçou, então ele age no pai do radio ou num checkbox que carregue o próprio /FT; um widget filho endereçado sozinho, com /FT só no pai, não é reconciliado por esse caminho. O HPDFReconcileChoiceSelection trata um único valor escalar e escreve no máximo um índice; list boxes de multi-seleção com várias entradas escolhidas estão fora do que o SetFormFieldValue modela. Nenhuma das duas rotinas valida o valor que você passa contra /Opt ou contra as chaves de estado on, então um erro de digitação produz um checkbox Off ou um combo sem índice em vez de uma exceção. E GetFormFieldValue devolve o texto /V guardado como ele está no dicionário, o que para um valor codificado em hex significa a grafia hexadecimal, não o texto decodificado
Uma vez que os valores estão dentro e as aparências pintadas, os dois próximos passos naturais ficam dos dois lados desta operação. Trocar dados de formulário com sistemas externos em massa, em vez de uma chamada a SetFormFieldValue por vez, é o que o import e export de XFDF no Delphi cobre. E quando o formulário preenchido está final e não deve mais ser editável, o flatten de campos AcroForm e XFA no Delphi assa exatamente os estados /AS e os appearance streams descritos aqui dentro do conteúdo estático das páginas, e é por isso que deixá-los consistentes antes do flatten não é opcional
A API de edição de formulário carregado deste artigo, incluindo SetFormFieldValue, EnsureLoadedFieldAppearanceStream e o grafo de recálculo incremental, vem como parte do HotPDF Delphi Component para Delphi e C++Builder