Um valor de campo de formulário PDF "reverte" quando o campo é guardado como dicionário direto: o PDFium promove a cópia no array /Annots da página a objeto indireto, enquanto o /Fields mantém uma segunda cópia direta. O PDFium Component for Delphi trata escritas de campos tipadas unificando ambas as referências num único dono indireto antes de escrever
O sintoma normalmente lê-se como um assombro. A escrita devolve True, e a leitura imediatamente seguinte entrega o valor antigo. Salve, reabra, e o valor antigo continua lá. Nada lançou, e o PDF parece perfeitamente comum em todos os viewers que possui
O sintoma: uma escrita bem-sucedida que não cola
O padrão de falha é estreito mas muito repetível: só aparece em formulários cujos dicionários de campo são escritos inline (como objetos diretos) em vez de objetos indiretos numerados. A maioria das ferramentas de autoria emite campos indiretos, por isso o problema esconde-se durante meses e depois aparece em ficheiros de um gerador específico, muitas vezes um exportador artesanal que escreve o /AcroForm inteiro inline
Duas observações juntas apontam para a causa: a escrita reporta sucesso, e uma leitura nativa do campo através da camada de formulário devolve o valor anterior. Quando vir ambos, pare de suspeitar da sua codificação de strings e comece a olhar para a posse de objetos
Porque é que um valor de campo de formulário PDF reverte após uma escrita bem-sucedida?
Um valor de campo de formulário PDF reverte porque dois dicionários separados reclamam ambos ser o mesmo campo, e a escrita atualizou a cópia que a árvore de campos não consulta. O valor de um campo vive na sua entrada /V, e o ISO 32000-1 §12.7.3.1 define onde essa entrada é autoritativa: no dicionário de campo alcançado a partir do /AcroForm /Fields, descendo pelo /Kids até ao campo terminal, com entradas herdáveis resolvidas a partir do /Parent
A mesma cláusula admite uma conveniência que causa todo o incómodo. Quando um campo tem exatamente uma anotação de widget, o dicionário de campo e o dicionário de anotação de widget podem ser fundidos num único dicionário. Num ficheiro bem formado esse dicionário fundido é um objeto indireto, referenciado de dois lugares: do /Fields (ou do /Kids de um pai) e do array /Annots da página. Duas referências, um objeto, um /V
Duas cópias de um dicionário
O ISO 32000-1 §7.3.10 é explícito em que só um objeto indireto tem identidade que pode ser partilhada. Um dicionário direto não tem número de objeto, por isso quando um gerador escreve o dicionário campo/widget fundido inline tanto no /Fields como no /Annots, o ficheiro não contém um objeto referenciado duas vezes. Contém dois dicionários de aparência igual que acontecem ter as mesmas chaves e valores
O PDFium tolera isto, e ao fazê-lo torna a divisão permanente. Quando precisa de um handle para a anotação, o PDFium pode promover o dicionário de widget direto no array /Annots da página a objeto indireto, mas a cópia direta equivalente fica onde estava dentro do /AcroForm /Fields (ou dentro do /Kids do pai). A partir desse momento o documento segura dois donos para um campo lógico. Um writer que edite o /V na anotação da página obtém um sucesso genuíno ao nível do dicionário. A árvore de campos, que é o que a camada de formulário e todos os leitores posteriores tratam como fonte de verdade, nunca vê a mudança
É por isso que o valor velho sobrevive a uma gravação e reabertura: o novo valor foi serializado fielmente num objeto que a hierarquia de campos não possui
Como o PDFium Component restaura um único dono de campo
O PDFium Component normaliza a posse direta de campos antes de cada escrita tipada: promove os dicionários /AcroForm e de campos diretos a objetos indiretos, liga cada widget direto à sua anotação de página correspondente, e reescreve /Fields, /Kids, /Parent e /Annots para que cada referência aponte para o mesmo objeto indireto. Só então muda o /V, por isso o valor do campo, o widget selecionado e o documento salvo partilham um único dono
A parte difícil é a identidade. Dois dicionários diretos com conteúdo idêntico não provam que são o mesmo widget; um formulário pode legitimamente conter dois widgets parecidos. O normalizador por isso recusa-se a adivinhar, e exige um nome de anotação /NM explícito e único (ISO 32000-1 §12.5.2) em cada widget direto. As regras aplicadas pelo caminho de escrita são estritas:
- Um widget direto precisa de um
/NMnão vazio de no máximo 1024 unidades UTF-16, e exatamente uma anotação de página pode carregar o mesmo nome - A cópia direta e a anotação da página têm de serializar identicamente fora o
/Parente o/APregenerado pelo PDFium; qualquer outra diferença rejeita a escrita - Ambas as ligações de pai são verificadas separadamente, por isso um
/Parentcorreto na anotação da página não pode mascarar um conflitante na cópia direta original - Um
/Parentem falta é fornecido só quando a posse única da árvore de campos torna a resposta inequívoca, e cópias de pais diretas equivalentes são reatadas ao seu dono promovido - Nomes de campo totalmente qualificados duplicados, árvores de campos cíclicas ou de posse múltipla e campos de assinatura rejeitam antes de qualquer coisa ser publicada
- Anotações não relacionadas, como links na mesma página, e entradas
/AcroFormnão relacionadas são mantidas intocadas
O normalizador também corre sob orçamentos duros: 4096 visitas de árvore de campos, páginas ou membros de array, profundidade de aninhamento 64, 256 novos objetos indiretos, 1 MiB por dicionário promovido, 4 MiB de bytes promovidos no total, 128 MiB de bytes de dicionário inspecionados, e 4096 unidades UTF-16 por nome de campo completo com 262144 no formulário. Um ficheiro hostil ou partido atinge um orçamento e falha com uma mensagem de erro em vez de transformar um preenchimento de formulário numa negação de serviço
A operação inteira é transacional. O TPdf.SetFormFieldValues captura uma fotografia, constrói uma atualização incremental contendo os objetos normalizados e os novos valores, carrega esse candidato, e só substitui o documento vivo depois de o candidato verificar. Se a normalização rejeitar o ficheiro, ou um pedido posterior num batch falhar, o documento original, os seus valores e o seu cursor lógico de campos ficam exatamente como estavam
Escrever e verificar um valor de campo direto em Delphi
O padrão seguro é escrever, depois ler de volta através da camada nativa de formulário, e tratar uma divergência como falha mesmo quando a escrita devolveu True. O TPdf.SetFormFieldValue endereça um campo por índice na página atual, recebe um TPdfFormValue tipado e enche um TPdfFormValueWriteReport; o TPdf.FormField[Index] lê o valor de volta através da camada de formulário do PDFium, que resolve a árvore de campos em vez de confiar no widget
uses
System.SysUtils, PDFium;
procedure WriteTextField(Pdf: TPdf; PageNo, FieldIndex: Integer;
const NewText: WString);
var
Value: TPdfFormValue;
Report: TPdfFormValueWriteReport;
begin
Pdf.PageNumber := PageNo; // SetFormFieldValue mira a página atual
Value := Default(TPdfFormValue);
Value.Kind := fvkText;
Value.Text := NewText;
if not Pdf.SetFormFieldValue(FieldIndex, Value, Report) then
raise Exception.CreateFmt('Field %d on page %d rejected: %s',
[FieldIndex, PageNo, Report.ErrorMessage]);
// Verificar contra a árvore de campos, não contra a nossa própria variável
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Num ficheiro com posse irresolvível, por exemplo um widget direto sem /NM, o SetFormFieldValue devolve False, o Report.UpdatedWidgets é zero e o Report.ErrorMessage nomeia o problema, como "Direct widgets require an explicit unique NM identity". Esse é o comportamento que quer: uma rejeição clara em vez de uma meia-escrita silenciosa. Mais uma precondição que vale a pena conhecer. A escrita tipada é só de anotações e não adquire focus nativo, por isso se um editor de campo nativo estiver ainda com focus a transação inteira rejeita antes de a fotografia ser capturada; chame Pdf.ClearFormFieldFocus deliberadamente primeiro em vez de repetir em loop
Para vários campos, sobretudo entre páginas, use a forma em batch. O TPdf.SetFormFieldValues aceita 1 a 256 pedidos, aplica todos a um candidato, e publica tudo ou nada. O TPdfFormValuesWriteReport.FailedRequest diz-lhe que pedido partiu o batch
procedure FillInvoiceHeader(Pdf: TPdf);
var
Requests: TPdfFormValueRequests;
Batch: TPdfFormValuesWriteReport;
begin
SetLength(Requests, 2);
Requests[0] := Default(TPdfFormValueRequest);
Requests[0].Page := 1;
Requests[0].FieldIndex := 0;
Requests[0].Value.Kind := fvkText;
Requests[0].Value.Text := 'ACME Tooling Inc.';
Requests[1] := Default(TPdfFormValueRequest);
Requests[1].Page := 2;
Requests[1].FieldIndex := 0;
Requests[1].Value.Kind := fvkText;
Requests[1].Value.Text := '2026-10-27';
if not Pdf.SetFormFieldValues(Requests, Batch) then
raise Exception.CreateFmt('Request %d failed: %s',
[Batch.FailedRequest, Batch.ErrorMessage]);
// Batch.UpdatedFields = 2; Batch.UpdatedWidgets conta cada widget sincronizado
end;
Dois pedidos a endereçar o mesmo campo terminal são rejeitados como dono duplicado em vez de deixar a última escrita ganhar, e o documento fonte é preservado. Valores de checkbox e rádio seguem as mesmas regras; a divisão /V contra /AS para botões está coberta em valor de campo de checkbox contra estado de widget no PDFium
Provar que o valor sobrevive a gravação e reabertura
Uma leitura de volta na mesma sessão prova que o documento vivo é coerente. Uma gravação e reabertura prova que o ficheiro é, e a verificação é barata o suficiente para manter nos seus próprios testes de integração
procedure SaveAndReopenCheck(Pdf: TPdf; const Target: string;
PageNo, FieldIndex: Integer; const Expected: WString);
var
Reopened: TPdf;
begin
if not Pdf.SaveAs(Target, saNoIncremental, pvUnknown) then
raise Exception.Create('Save failed');
Reopened := TPdf.Create(nil);
try
Reopened.FileName := Target;
Reopened.Active := True;
Reopened.PageNumber := PageNo;
if Reopened.FormField[FieldIndex] <> Expected then
raise Exception.Create('Saved file lost the field value');
finally
Reopened.Free;
end;
end;
Mais duas armadilhas ao nível do byte: Trim e o /ID do trailer
Normalizar campos diretos significa reler bytes crus de dicionário, o que traz duas armadilhas menores relevantes para qualquer código Delphi que toque em sintaxe PDF
A primeira é o SysUtils.Trim sobre bytes PDF crus guardados num AnsiString. Uma string literal como uma opção de choice pode legitimamente conter texto UTF-16BE com uma marca de ordem de bytes FE FF e bytes NUL embutidos. No Delphi Unicode, o SysUtils.Trim recebe um string, por isso a chamada converte silenciosamente o AnsiString para UTF-16 usando a página de código ANSI ativa e converte o resultado de volta. Consoante essa página de código, a ida e volta pode reescrever bytes altos, danificar a BOM e até perturbar o delimitador que se segue. A abordagem segura mantém o lexer nos bytes: salte whitespace PDF por posição (o ISO 32000-1 §7.2.2 lista NUL, tab, LF, FF, CR e espaço) e corte com Copy, descodificando só quando um valor se sabe ser uma string de texto PDF
// Arriscado em bytes PDF crus: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Seguro em bytes: saltar whitespace PDF por posição, depois cortar
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
A segunda armadilha assenta na própria atualização incremental. O ISO 32000-1 §7.5.5 (Tabela 15) e §14.4 exigem que o /ID do trailer seja um array de duas strings de bytes. O helper interno que compõe o identificador da atualização esparsa devolve só o conteúdo das duas strings, sem a chave /ID nem os parênteses retos do array, e o trailer tem de o envolver explicitamente como /ID [ ... ]. O loader tolerante do PDFium, e uma regravação completa posterior, ambos escondem um wrapper em falta, por isso a verificação fiável é analisar os ficheiros candidato e salvo com um parser estrito independente e procurar duas strings de 16 bytes. Se escrever atualizações incrementais você mesmo, a mesma armadilha aplica-se; o artigo de atualização incremental para PDFs encriptados cobre a cadeia do trailer com mais profundidade
O que deve fazer no seu próprio código de formulários Delphi?
- Leia sempre o valor de volta com
FormField[Index]após uma escrita, e nos testes salve, reabra e leia outra vez - Trate
FalsemaisErrorMessagecomo informação: uma rejeição que nomeieNM, um conflito de pais ou um nome duplicado significa que a posse do ficheiro é genuinamente ambígua - Se gera PDFs, emita campos e widgets como objetos indiretos e dê a cada widget um
/NMúnico; isso remove a ambiguidade na fonte - Use
SetFormFieldValuespara preenchimentos multi-campo ou entre páginas para que uma falha tardia não possa deixar metade do formulário atualizado - Nunca corra
Trimou outros helpersstringda RTL sobre bytes PDF crus guardados emAnsiString
Seja claro quanto às fronteiras, também. Documentos assinados, formulários XFA, fotografias encriptadas e ficheiros cuja posse de widgets não pode ser resolvida de forma única ficam fora do perfil declarado e rejeitam em vez de serem forçados a passar. Se o seu fluxo move campos entre documentos, as questões de posse tornam-se ainda mais difíceis; enxertar campos AcroForm entre documentos PDF trata desse caso
O PDFium Component traz esta escrita de formulários tipada e transacional a aplicações Delphi, C++Builder e Lazarus ao lado de renderização, extração de texto e edição de documentos. Histórico de versões, licenciamento e downloads estão na página do produto PDFium Component for Delphi