O HotPDF Delphi Component preenche um campo AcroForm existente num PDF carregado através do THotPDF.SetFormFieldValue, endereçado quer por índice de campo baseado em zero quer por nome de campo totalmente qualificado. Escrever a nova entrada /V é a parte fácil; o que torna a chamada fiável em formulários do mundo real é que o mesmo método mantém também coerentes três peças de estado que são invisíveis até correrem mal: a identidade descodificada do campo, para que um nome não ASCII possa sequer ser encontrado, o estado de aparência /AS nos widgets de checkbox e de rádio, e o array de índices de seleção /I nos campos de escolha. O appearance stream visível é um passo separado e explícito, através do EnsureLoadedFieldAppearanceStream
O cenário é o mais banal: um cliente envia-lhe o formulário dele, uma declaração de impostos, um pedido de seguro, uma nota de encomenda que alguém construiu no Acrobat há anos, e a sua aplicação Delphi tem de o preencher a partir de uma base de dados e devolver um ficheiro que abra corretamente em todo o lado. Não tem qualquer controlo sobre como o formulário foi criado. Os nomes de campo podem estar codificados em UTF-16, os valores de exportação de checkboxes podem ser 2 em vez de Yes, e as combo boxes podem usar pares de opções [export display]. Cada um desses detalhes tem uma regra na ISO 32000-1, e cada regra é algo que o SetFormFieldValue agora trata por si. Este artigo é sobre o que ele faz, porque o faz, e onde para. Para o problema irmão de criar campos que ainda não existem, veja acrescentar campos AcroForm a um PDF carregado no Delphi
Porque é que o SetFormFieldValue não encontra um campo com nome não ASCII?
Antes da v2.752.1 a resposta era a codificação: o campo vivia no ficheiro sob um nome UTF-16BE hexadecimal, e a cache de nomes guardava a grafia hexadecimal em vez do texto. A ISO 32000-1 §12.7.3.1 define o nome de campo parcial /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 à frente. As ferramentas de criação serializam rotineiramente esses nomes como strings hexadecimais segundo a §7.3.4.3, pelo que um campo chamado Straße chega como <FEFF005300740072006100DF0065>. Dentro do HotPDF, o THPDFStringObject.Value contém o texto hexadecimal em bruto sempre que IsHexadecimal está definido, que é exatamente o que quer para uma ida e volta sem perdas do dicionário original e exatamente o que não quer como chave de pesquisa. O HPDFLoadedFormTextName separa as duas preocupações. Quando a cache de relações é construída, cada valor /T passa por lá: se o objeto string for hexadecimal, o HPDFHexToBytes restaura a sequência de bytes; se os bytes começarem por FE FF e tiverem comprimento par, a carga é descodificada como UTF-16BE e re-codificada como UTF-8; o resultado é depois unido ao nome do pai com um ponto para formar o nome totalmente qualificado que a §12.7.3.1 descreve, pelo que um filho chamado City sob um pai chamado Address fica registado como Address.City. A chave de cache é normalizada para minúsculas, o que faz com que SetFormFieldValue('address.city', ...) também resulte; é uma conveniência para lá da norma, já que a especificação trata os nomes como sensíveis a maiúsculas. Crucialmente, só a chave de cache muda. O objeto /T no dicionário do campo mantém a sua codificação hexadecimal, pelo que gravar o documento não reescreve a identidade de um campo que se limitou a preencher
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('claim-form.pdf') <= 0 then Exit;
// Os nomes qualificados são descodificados de strings /T em UTF-16BE e
// unidos com pontos, para que nomes aninhados e não ASCII resolvam
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 uma string hexadecimal de PDF
Pdf.SetFormFieldValue('Applicant.City', 'FEFF004D00FC006E006300680065006E');
Pdf.SaveLoadedDocument('claim-form-filled.pdf');
finally
Pdf.Free;
end;
end;
O que escreve afinal o SetFormFieldValue?
Ambos os overloads correm os mesmos cinco passos: localizar o dicionário do campo, escrever o /V através do HPDFSetDictFormValue, reconciliar os índices de seleção de escolha, marcar o dicionário como sujo, reconciliar os estados de aparência dos botões, e por fim registar o índice do campo através do NoteLoadedFormFieldDirty. Esse último passo importa se o formulário transportar scripts de cálculo, porque o conjunto de sujos é o que o overload sem parâmetros RecalculateLoadedFormFieldsIncremental consome para voltar a correr apenas os cálculos que leem transitivamente um campo alterado. O HPDFSetDictFormValue em si é cuidadoso quanto ao tipo de objeto que substitui. Se o /V existente for um objeto name, que é o que os campos de checkbox e de rádio usam para o seu valor de exportação, o novo valor é escrito como name, nunca como string, porque os names de PDF são só ASCII por construção. Caso contrário escreve um objeto string e inspeciona o valor que passou: uma string que começa por FEFF, tem comprimento par e é composta apenas por dígitos hexadecimais é tratada como a forma de transmissão UTF-16BE da §7.9.2.2 e guardada com IsHexadecimal definido, pelo que se serializa como <FEFF...> e não como um (FEFF...) literal. É esse o mecanismo em que a linha do City acima assenta; qualquer outra string é guardada como uma string literal com os bytes que deu, por isso para texto latino simples passa texto simples
Porque é que um checkbox mantém o visto antigo depois de o valor mudar?
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 transporta um estado de aparência /AS que nomeia qual dos streams em /AP /N está a ser mostrado naquele momento, e os visualizadores pintam a partir do /AS, não do /V. Se mudar o /V para Yes mas deixar o /AS em Off, o ficheiro fica internamente contraditório, e a achatagem vai cozer alegremente a aparência obsoleta de não marcado na página, enquanto os dados do formulário dizem marcado. O ReconcileLoadedButtonAppearanceStates existe para fechar essa lacuna: para um campo cujo /FT é Btn, visita o próprio dicionário do campo e cada entrada do seu array /Kids, lê o nome do estado on a partir de /AP /N, e reescreve o /AS para esse nome quando corresponde ao valor do campo ou para Off quando não corresponde
Dois detalhes de formulários reais moldaram a correção da v2.752.3. Primeiro, é permitido que um dicionário de aparência normal contenha apenas o estado on; a §12.7.4.2.3 nomeia a aparência off Off mas as ferramentas de criação omitem frequentemente o seu stream e deixam o visualizador desenhar nada. O código anterior desistia quando o dicionário tinha menos de duas entradas, pelo que esses checkboxes de estado único mantinham silenciosamente o visto antigo. A verificação é 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. Os formulários reais usam 2, Yes, On ou uma palavra localizada, por isso a comparação é contra a chave real, sem distinção de maiúsculas, nunca contra um Yes cravado no código. Os botões de rádio acrescentam mais uma complicação, descrita na §12.7.4.2.4: a seleção vive no /V do campo pai, enquanto os filhos individuais detêm os widgets e habitualmente não têm /V próprio. O helper aninhado InheritedButtonValue sobe portanto a cadeia /Parent, até 64 níveis, até encontrar um valor não vazio, para que cada filho seja comparado com o valor do grupo a que pertence. Pôr o pai no valor de exportação de um filho liga exatamente esse filho e desliga todos os irmãos
// Checkbox: o valor de exportação tem de corresponder à chave de estado on em /AP /N
// (frequentemente 'Yes', mas formulários reais usam '2', 'On' ou outra coisa qualquer)
Pdf.SetFormFieldValue('Consent', 'Yes');
// Grupo de rádio: o /V é escrito no pai; cada widget filho recebe
// o /AS posto ao seu próprio nome de exportação ou a Off
Pdf.SetFormFieldValue('PaymentMethod', 'Card');
// Limpar um checkbox: qualquer valor que não corresponda a nenhum estado on dá /AS Off
Pdf.SetFormFieldValue('Newsletter', 'Off');
Campos de escolha: manter o /I em passo com o /V
Numa combo box ou list box, o /V não é o único sítio onde uma seleção fica registada. A Tabela 231 da §12.7.4.4 define o /I como um array de índices baseados em zero para dentro de /Opt que identifica os itens selecionados, e um visualizador que encontre o /I a apontar para a opção 0 enquanto o /V nomeia a opção 3 pode realçar a linha errada. Desde a v2.754.1, o HPDFReconcileChoiceSelection corre dentro de cada chamada a SetFormFieldValue e, quando o /FT herdado é Ch, reconstrói o /I a partir do novo valor. A ordem das operações é deliberada. A entrada local /I é apagada primeiro, sem tocar no seu conteúdo: se o array antigo fosse um objeto indireto partilhado com outro campo, alterá-lo no local corromperia a seleção do outro campo, por isso a rotina larga a referência e cria um array direto novo. Resolve depois o /Opt através da cadeia /Parent, já que as opções de escolha podem ser herdadas, e varre as entradas. Uma opção que é uma string simples é comparada diretamente; um par [export display] é comparado pelo seu elemento de exportação, e um par com menos de dois elementos é saltado. Ambos os lados passam pelo HPDFLoadedFormTextName, para que uma opção hex UTF-16 corresponda a um valor hex UTF-16 sem que os tenha de escrever de forma idêntica. Na primeira correspondência é escrito um /I de um elemento e a varredura para; um valor escalar substitui sempre qualquer multi-seleção anterior, independentemente do flag MultiSelect
Quando nada corresponde, não é escrito /I nenhum. Esse é o resultado correto para uma combo box editável, onde a §12.7.4.4 permite que o utilizador escreva um valor fora da lista de opções; um valor desses não tem índice, e um índice obsoleto seria pior do que nenhum. É também o que obtém se passar uma etiqueta de apresentação em vez de um valor de exportação a uma lista de opções emparelhadas, por isso, quando uma combo box se recusar a mostrar a sua seleção, verifique qual das metades do par forneceu
// /Opt é [[US United States] [CA Canada] [MX Mexico]]:
// corresponder ao valor de exportação, e o /I passa a [1]
Pdf.SetFormFieldValue('Country', 'CA');
// Combo editável com um valor fora de /Opt: o /V é escrito,
// o /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 escolha. Depois da chamada, o /V contém o texto novo enquanto o /AP /N continua a pintar o antigo, e qual dos dois um visualizador mostra depende de o dicionário AcroForm transportar /NeedAppearances true segundo a §12.7.3.3 e de o visualizador o honrar. Se precisa que o ficheiro desenhe o novo valor em todos os leitores, incluindo achatadores e geradores de miniaturas que ignoram o flag, chame o EnsureLoadedFieldAppearanceStream com o índice do campo. Constrói um Form XObject a partir da string /DA herdada, do quadding /Q, do layout comb /MaxLen e do valor, resolve o tipo de letra nomeado através dos recursos /DR do AcroForm para que um tipo de letra Type0 mantenha o seu próprio tipo de letra descendente em vez de degradar para Helvetica, e devolve True quando pelo menos um widget recebeu um stream. O overload por nome do SetFormFieldValue não lhe devolve nenhum índice, por isso obtenha um através do GetFormField, que devolve um THPDFLoadedFormField que lhe pertence e que tem de libertar. A suite de regressão da alteração da v2.752.1 é explícita quanto a esta separação: define um valor, chama o EnsureLoadedFieldAppearanceStream, desenha depois a página e verifica que os píxeis 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 utilizador vai ver
var
Field: THPDFLoadedFormField;
begin
Pdf.SetFormFieldValue('Applicant.FullName', 'Maria Schneider');
Field := Pdf.GetFormField('Applicant.FullName');
try
// Pintar o novo valor no /AP para que os visualizadores que ignoram
// o /NeedAppearances o mostrem na mesma
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 a pena conhecer antes de construir sobre isto
O ReconcileLoadedButtonAppearanceStates testa o /FT local do dicionário que endereçou, pelo que atua sobre o pai de rádio ou sobre um checkbox que transporte o seu próprio /FT; um widget filho endereçado por si só, com o /FT apenas no seu pai, não é reconciliado por essa via. O HPDFReconcileChoiceSelection trata um único valor escalar e escreve no máximo um índice; as list boxes de multi-seleção com várias entradas escolhidas estão fora do que o SetFormFieldValue modela. Nenhuma das rotinas valida o valor que passa contra o /Opt ou contra as chaves de estado on, por isso um erro de escrita produz um checkbox em Off ou uma combo sem índice em vez de uma exceção. E o GetFormFieldValue devolve o texto /V guardado tal como está no dicionário, o que para um valor codificado em hexadecimal significa a grafia hexadecimal, não o texto descodificado
Depois de os valores estarem lá dentro e as aparências pintadas, os dois passos naturais seguintes ficam de um lado e do outro desta operação. Trocar dados de formulário com sistemas externos em bloco, em vez de uma chamada a SetFormFieldValue de cada vez, é o que a importação e exportação XFDF no Delphi cobre. E quando o formulário preenchido está final e já não deve ser editável, achatar campos AcroForm e XFA no Delphi coze exatamente os estados /AS e os appearance streams aqui descritos em conteúdo estático de página, e é por isso que os deixar coerentes antes de achatar não é opcional
A API de edição de formulários carregados deste artigo, incluindo o SetFormFieldValue, o EnsureLoadedFieldAppearanceStream e o grafo de recálculo incremental, sai como parte do HotPDF Delphi Component para Delphi e C++Builder