Um formulário de pesquisa mostra duas listas drop-down no Acrobat, group.a e group.b. Percorra /AcroForm /Fields com código que trata qualquer nó com /FT próprio como um campo completo, e a contagem volta como 1: um único campo chamado group com dois widgets. Pergunte por group.b pelo nome e a busca falha. Abra o dicionário do filho e não há /FT nenhum nele, nem /Ff, nem lista de opções. Nada está quebrado. O tipo, os flags e as opções ficam um nível acima, no pai, e a especificação PDF permite explicitamente que vivam lá
O layout é legal e econômico. Uma ferramenta de autoria grava um nó de grupo com /FT /Ch, /Ff 131078 e um array /Opt uma vez, e depois pendura dois filhos nomeados embaixo, cada um um dicionário mesclado de campo-plus-widget contendo apenas /T, /Parent, /Rect e seu próprio /V. Ler tal formulário corretamente exige três coisas: seguir /Parent para toda chave herdável, decidir onde a árvore de campos realmente termina, e garantir que um reset escreva um valor do tipo PDF certo. Este texto retoma de onde definir valores de campos de formulário em um PDF carregado com Delphi para
Quais entradas AcroForm um campo pode herdar do pai dele?
A ISO 32000-1 §12.7.3.1 (Tabela 220) marca /FT, /Ff, /V e /DV como herdáveis, e a Tabela 229 na §12.7.4.3 faz o mesmo para o /MaxLen de um campo de texto. Arrays de opções recebem o mesmo tratamento aqui, porque produtores que compartilham uma lista de escolhas entre filhos armazenam /Opt no grupo. Um leitor que só verifica o dicionário local vai, portanto, reportar o tipo errado, flags 0 e um valor vazio para um filho perfeitamente válido
O HotPDF Delphi Component responde cada uma dessas leituras da mesma forma. Ele procura a chave no dicionário do próprio campo e, se a chave está lá, resolve a referência indireta e para. Caso contrário, segue /Parent e tenta de novo, por no máximo 128 níveis. Esse teto não é enfeite. Ponteiros /Parent em arquivos danificados podem formar um loop que não tem nada a ver com /Kids, e um leitor que confia neles incondicionalmente trava no primeiro arquivo desses que encontrar
Uma regra desse caminho é fácil de entender ao contrário: o primeiro dicionário que contém a chave vence, mesmo quando o valor armazenado lá é uma string vazia. Um /V () local no group.b é o autor dizendo "este filho está em branco", e o caminho para ali em vez de subir para um valor que o pai possa ter. Tratar vazio como "ausente" faria um filho limpo exibir a seleção do pai, o oposto do que o autor escreveu
Os getters públicos todos se apoiam nessa busca: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue, e os auxiliares de opções GetLoadedFormFieldOptionCount e GetLoadedFormFieldOptions. Ler o filho do grupo da pesquisa não precisa de código especial:
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' carrega /FT /Ch, /Ff 131078 e /Opt; o filho
// 'group.b' carrega só /T, /Parent, /Rect e o /V dele
Field := Pdf.GetFormField('group.b');
try
if Pdf.GetFormFieldType(Field.Index) = lfftChoice then
begin
// 131078 = Combo (bit 18) + NoExport (bit 3) + Required (bit 2)
Writeln(Pdf.GetLoadedFormFieldFlags(Field.Index));
Writeln(Pdf.IsFormFieldRequired(Field.Index)); // TRUE
Writeln(Pdf.GetLoadedFormFieldOptionCount(Field.Index));
Writeln(Pdf.GetFormFieldValue(Field.Index)); // o /V local
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
O GetFormField retorna um novo wrapper THPDFLoadedFormField a cada chamada e levanta uma exceção quando o nome é desconhecido: mantenha a chamada fora do bloco try do wrapper e libere o wrapper você mesmo
Descobrindo onde a árvore de campos termina
Um /FT local é o teste errado para um campo terminal, porque um pai pode fornecer o tipo e ainda ter campos filhos nomeados. A travessia tentadora para de descer sempre que um nó tem /FT próprio ou não tem /Kids. No formulário de pesquisa, group tem tanto /FT /Ch quanto /Kids, então essa travessia o registra como um campo com duas anotações de widget. Os nomes group.a e group.b desaparecem. A contagem de campos é 1. Uma busca pelo nome do filho falha, e uma escrita de valor só pode cair no pai compartilhado
O HotPDF olha os kids em vez disso. Um kid conta como campo filho se ele tem /T próprio, tem /Kids próprio, ou não é um dicionário /Subtype /Widget. Só quando nenhum kid se qualifica o nó é terminal, e seus kids são então tratados como seus widgets. Os dois casos de borda por trás dessa regra vêm de dicionários mesclados, que a §12.7.3.1 permite quando um campo tem exatamente um widget:
- Um dicionário mesclado nomeado carrega
/Subtype /Widgete ainda assim é um campo filho. O subtipo sozinho não pode empurrá-lo para a lista anônima de widgets do pai; o/Tdecide - Alguns produtores repetem o
/FTdo pai em todo widget anônimo. Então um/FTem um widget também não pode ser lido como "um novo campo começa aqui"
A mesma classificação dirige o índice de campos, o FormFieldExists e o RemoveFormField. Cada uma dessas caminhadas também registra os dicionários que já visitou e desiste depois de 128 níveis. Suponha que o grupo é o objeto 5 e se lista duas vezes entre os próprios kids: /Kids [5 0 R 5 0 R 6 0 R 7 0 R]. Ele ainda reporta exatamente dois campos, os filhos 6 e 7. Sem recursão infinita, e sem um terceiro campo fantasma vindo da autorreferência
Como o RemoveFormField evita apagar campos irmãos?
O RemoveFormField apaga apenas o filho que você nomeia porque descoberta e remoção compartilham uma definição de campo terminal. O overload por nome resolve um índice de campo a partir da lista de campos, depois percorre /AcroForm /Fields uma segunda vez contando campos terminais para achar o dicionário a apagar. Se a segunda caminhada usasse a regra tentadora do /FT enquanto a primeira usava a regra baseada em kids, os índices discordariam: a caminhada de remoção veria group como campo 0, e remover group.a levaria embora o pai, o irmão e os dois widgets
Então a caminhada de remoção aplica o mesmo teste de campo filho e o mesmo conjunto de visitados que a descoberta. Ela coleta as anotações de widget que pertencem ao filho removido, tira apenas essas do /Annots de cada página, e remove o pai apenas quando o array /Kids dele termina vazio. Com group.b ainda listado, o pai fica, e o tipo, os flags e as opções que group.b herda ficam com ele
// Remove um filho nomeado; o irmão dele e o pai compartilhado sobrevivem
Pdf.RemoveFormField('group.a');
// Checagens de sanidade que você mesmo adicionaria, não fazem parte da biblioteca
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipo, flags e opções continuam resolvidos pelo pai
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Quando você verifica uma remoção nos seus próprios formulários, olhe em três lugares, porque cada um pega um erro diferente: o /Kids do pai, o /Annots da página, e o valor e a aparência do irmão sobrevivente. Faça isso uma vez depois do SaveLoadedDocument e uma vez depois do SaveIncrementalUpdate, já que um salvamento incremental anexa apenas os objetos mudados e o /Kids do pai precisa estar entre eles
O que o ResetLoadedFormField copia de um /DV herdado
O ResetLoadedFormField escreve um /V local que é uma cópia fresca do /DV herdado, com o mesmo tipo de objeto PDF, e valida o default completo antes de tocar no campo. O default de um checkbox é um name como /Yes. O default de um list box multi-select é um array de strings. Um default de texto pode ser uma string UTF-16 hexadecimal como <FEFF...>. Passe qualquer um deles de volta por GetLoadedFormFieldDefaultValue e SetFormFieldValue e você recebe uma string onde um name pertence, uma string vazia onde havia um array, ou dígitos hex literais
O reset, portanto, ramifica conforme o que o /DV herdado realmente é:
- Campos de texto e choice com um default de string recebem um novo objeto string que mantém o flag hexadecimal do original
- Campos choice com um default de array recebem um novo array de novas strings; um array que contém qualquer coisa que não seja string levanta uma exceção
- Checkboxes e radio buttons recebem um novo objeto name
- Um default de qualquer outro tipo levanta uma exceção, e
/Ve/Ificam exatamente como estavam
Copiar em vez de referenciar é deliberado, e é a única opção segura. Um /V de filho que apontasse para o array /DV do pai compartilharia um objeto entre "valor atual" e "valor default"; o próximo SetFormFieldValue nesse filho reescreveria silenciosamente o default de todo irmão. Alguns bytes extras por campo de reset é um preço barato por manter os dois separados. Pushbuttons, que não têm valor (Tabela 226, bit 17), e campos de assinatura ficam em um caminho simples que escreve uma cópia em string do próprio /DV deles
Quando não existe /DV em lugar nenhum subindo pela cadeia, o método escreve uma string vazia local, ou /Off para um campo checkbox ou radio. Apagar o /V do filho pareceria mais arrumado e daria a resposta errada. Se o pai guarda um valor atual, remover a sobreposição do filho traz esse valor de volta direto, e o campo "resetado" agora mostra algo que o usuário nunca digitou nele
var
Field: THPDFLoadedFormField;
begin
// O pai 'palette' é um list box MultiSelect com
// /Opt [(b) (g) (r)] e /DV [(b) (r)]; o filho guarda apenas /T e /V
Field := Pdf.GetFormField('palette.primary');
try
try
Pdf.ResetLoadedFormField(Field.Index);
// O filho agora tem seu próprio /V [(b) (r)] e /I [0 2];
// o array /DV do pai permanece intocado
except
on E: Exception do
// Default malformado (tipo errado, item de array não string):
// /V e /I foram deixados como estavam
Writeln('Reset skipped: ', E.Message);
end;
// O getter escalar não consegue representar um default de array
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // vazio
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Um reset de campo único também é uma coisa diferente da ação ResetForm da §12.7.5.3, que um visualizador executa sobre um conjunto de campos quando o usuário clica em um botão; essa ação é coberta em construindo campos e ações AcroForm com HotPDF. O ResetLoadedFormField edita um campo carregado, segue a própria regra dele para o caso sem default, e marca o campo como mudado para que o recálculo incremental o pegue
Mantendo /V, /I e /AS em acordo
Um reset só está correto se o índice de seleção e o estado de aparência seguirem o valor, e o ResetLoadedFormField termina com os mesmos dois passos de acompanhamento do SetFormFieldValue. Para campos choice, ele remove o /I local do filho, casa cada valor com a metade de export de cada entrada /Opt, e grava um novo /I ordenado. Resetar para [(b) (r)] contra as opções b, g, r rende /I [0 2]. O /I herdado do pai, se houver, permanece intocado
Botões funcionam a partir do tipo herdado. Um checkbox filho cujo /FT /Btn mora no pai ainda tem o /AS atualizado. No lado da escrita, SetFormFieldValue e SetLoadedFormFieldDefaultValue armazenam um objeto name para um botão herdado que não é pushbutton mesmo quando o filho não tem entrada local da qual copiar o tipo. E quando o EnsureLoadedFieldAppearanceStream reconstrói aparências de botões, ele grava /AS /Off a menos que o valor case com o estado ligado, e dá a cada state stream /Type /XObject, /Subtype /Form e um /BBox. Sem essa checagem de /Off, regenerar aparências depois de um reset mostraria a caixa marcada de novo antes de o arquivo ser sequer salvo
Getters escalares e outras bordas para planejar em volta
Os getters de string continuam escalares, e código que compara a saída deles deve saber o que eles achacam. GetFormFieldValue e GetLoadedFormFieldDefaultValue retornam uma string vazia para um valor de array, transformam números e booleanos em 42 ou true, e reportam uma string codificada em hex em sua grafia hexadecimal. Se você precisa saber se um default de list box guarda duas seleções ou nenhuma, o getter de default não pode dizer: ambas voltam como string vazia
Um ciclo de /Parent encerra a busca sem levantar, então um campo cujo tipo se perdeu dentro de um ciclo reporta lfftUnknown e flags 0. Trate lfftUnknown em um campo nomeado como sinal de um arquivo danificado que vale registro em log. SetFormFieldValue e ResetLoadedFormField sempre escrevem o filho que você endereça e nunca promovem um valor para o pai compartilhado. Isso é o certo para filhos independentes como as duas drop-downs da pesquisa. Radio groups são diferentes: a seleção pertence ao campo que possui o grupo, então enderece esse campo em vez de um dos kids dele
Cada chamada também commita um campo por conta própria. Um loop de resets sobre vinte campos são vinte edições independentes; se a quinta levanta em um default malformado, as quatro primeiras já foram aplicadas. Para trabalho em lote, leia o tipo e o default de todos os alvos primeiro, decida quais campos você vai tocar, e só então comece a escrever
A busca de atributos herdados, a classificação da árvore de campos baseada em kids e o reset tipado pertencem todos à API de formulários carregados do HotPDF Delphi Component, que também cobre projetos Delphi e C++Builder que precisam adicionar novos campos a um documento existente, como mostrado em adicionar campos AcroForm a um PDF carregado no Delphi