Um valor de campo de formulário PDF "reverte" quando o campo é armazenado como dicionário direto: o PDFium promove a cópia no array /Annots da página a um objeto indireto, enquanto o /Fields mantém uma segunda cópia direta. O PDFium Component for Delphi trata gravações tipadas de campos unificando as duas referências num único dono indireto antes de gravar
O sintoma normalmente lê como um assombro. A gravação devolve True, e a leitura seguinte devolve o valor antigo. Salve, reabra, e o valor antigo continua lá. Nada lançou, e o PDF parece perfeitamente comum em todo viewer que você tem
O sintoma: uma gravação bem-sucedida que não gruda
O padrão de falha é estreito mas muito repetível: só aparece em formulários cujos dicionários de campos são gravados inline (como objetos diretos) em vez de como objetos indiretos numerados. A maioria das ferramentas de autoria emite campos indiretos, então o problema se esconde por meses e depois aparece em arquivos de um gerador específico, muitas vezes um exportador artesanal que grava o /AcroForm inteiro inline
Duas observações juntas apontam para a causa: a gravação reporta sucesso, e uma leitura nativa do campo pela camada de formulário devolve o valor anterior. Quando você vê as duas, pare de suspeitar da sua codificação de strings e comece a olhar a posse de objetos
Por que um valor de campo de formulário PDF reverte após uma gravação bem-sucedida?
Um valor de campo de formulário PDF reverte porque dois dicionários separados ambos alegam ser o mesmo campo, e a gravação atualizou a cópia que a árvore de campos não consulta. O valor de um campo vive na entrada /V dele, e a 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é o campo terminal, com entradas herdáveis resolvidas do /Parent
A mesma cláusula permite uma conveniência que causa todo o problema. 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 arquivo 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
A ISO 32000-1 §7.3.10 é explícita que só um objeto indireto tem uma identidade que pode ser compartilhada. Um dicionário direto não tem número de objeto, então quando um gerador grava o dicionário campo/widget fundido inline tanto no /Fields quanto no /Annots, o arquivo não contém um objeto referenciado duas vezes. Ele contém dois dicionários de aparência igual que por acaso têm as mesmas chaves e valores
O PDFium tolera isso, 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 um objeto indireto, mas a cópia direta equivalente fica onde estava dentro do /AcroForm /Fields (ou dentro do /Kids do pai). Daquele momento o documento tem dois donos para um campo lógico. Um writer que edita o /V na anotação da página obtém um sucesso genuíno no nível do dicionário. A árvore de campos, que é o que a camada de formulário e todo leitor posterior tratam como fonte da verdade, nunca vê a mudança
É por isso que o valor velho sobrevive a um save e reopen: o valor novo 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 toda gravação tipada: ele promove dicionários diretos /AcroForm e de campos a objetos indiretos, liga cada widget direto à anotação de página correspondente dele, e regrava /Fields, /Kids, /Parent e /Annots para que toda referência aponte para o mesmo objeto indireto. Só então ele muda o /V, para que o valor do campo, o widget selecionado e o documento salvo compartilhem um dono
A parte difícil é 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 então se recusa a chutar, 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 gravação 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 de página precisam serializar identicamente exceto por
/Parente o/APregenerado pelo PDFium; qualquer outra diferença rejeita a gravação - Os dois links de pai são conferidos separadamente, então um
/Parentcorreto na anotação de página não pode mascarar um conflitante na cópia direta original - Um
/Parentausente é fornecido só quando a posse única na árvore de campos torna a resposta inequívoca, e cópias diretas de pai equivalentes são religadas ao dono promovido delas - Nomes de campos 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 roda sob orçamentos rígidos: 4096 visitas da á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 completo de campo com 262144 pelo formulário. Um arquivo hostil ou quebrado esbarra num 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 um snapshot, constrói uma atualização incremental contendo os objetos normalizados e os novos valores, carrega esse candidato, e só substitui o documento vivo uma vez que o candidato verifica. Se a normalização rejeita o arquivo, ou uma requisição posterior num lote falha, o documento original, os valores dele e o cursor lógico de campos ficam exatamente como estavam
Gravando e verificando um valor de campo direto no Delphi
O padrão seguro é gravar, depois reler pela camada nativa de formulário, e tratar uma discrepância como falha mesmo quando a gravação devolveu True. O TPdf.SetFormFieldValue endereça um campo por índice na página atual, recebe um TPdfFormValue tipado e preenche um TPdfFormValueWriteReport; o TPdf.FormField[Index] lê o valor de volta pela 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]);
// Verifique 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 arquivo 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 você quer: uma rejeição clara em vez de uma meia gravação silenciosa. Mais uma pré-condição vale conhecer. A gravação tipada é apenas de anotação e não adquire foco nativo, então se um editor nativo de campo ainda está focado a transação inteira rejeita antes de o snapshot ser capturado; chame Pdf.ClearFormFieldFocus deliberadamente primeiro em vez de retentar num loop
Para vários campos, especialmente entre páginas, use a forma de lote. O TPdf.SetFormFieldValues aceita de 1 a 256 requisições, aplica todas a um candidato, e publica tudo ou nada. O TPdfFormValuesWriteReport.FailedRequest diz qual requisição quebrou o lote
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 todo widget sincronizado
end;
Duas requisições endereçando o mesmo campo terminal são rejeitadas como dono duplicado em vez de deixar a última gravação vencer, e o documento de origem é preservado. Valores de checkbox e radio seguem as mesmas regras; a divisão /V versus /AS para botões está coberta em valor de campo de checkbox versus estado de widget no PDFium
Provando que o valor sobrevive a save e reopen
Uma releitura na mesma sessão prova que o documento vivo é coerente. Um save e reopen prova que o arquivo é, e a checagem é barata o suficiente para manter nos seus 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 de nível de byte: Trim e o /ID do trailer
Normalizar campos diretos significa reler bytes brutos de dicionário, o que traz duas armadilhas menores relevantes para qualquer código Delphi que toque sintaxe PDF
A primeira é o SysUtils.Trim sobre bytes PDF brutos 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, então a chamada converte silenciosamente o AnsiString para UTF-16 usando a code page ANSI ativa e converte o resultado de volta. Dependendo dessa code page, a ida e volta pode reescrever bytes altos, danificar o BOM e até perturbar o delimitador que segue. A abordagem segura mantém o lexer em bytes: pule whitespace PDF por posição (a ISO 32000-1 §7.2.2 lista NUL, tab, LF, FF, CR e espaço) e corte com Copy, decodificando só quando um valor for sabidamente uma string de texto PDF
// Arriscado em bytes PDF brutos: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Seguro em bytes: pule whitespace PDF por posição, depois corte
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 fica na própria atualização incremental. A ISO 32000-1 §7.5.5 (Tabela 15) e a §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 colchetes do array, e o trailer precisa envolvê-lo explicitamente como /ID [ ... ]. O loader tolerante do PDFium, e um resave completo posterior, ambos escondem um wrapper ausente, então a checagem confiável é parsear os arquivos candidato e salvo com um parser estrito independente e procurar duas strings de 16 bytes. Se você grava atualizações incrementais por conta própria, a mesma armadilha se aplica; o artigo de atualização incremental para PDFs criptografados cobre a cadeia de trailers em mais profundidade
O que você deve fazer no seu próprio código de formulários Delphi?
- Sempre releia o valor com
FormField[Index]depois de uma gravação, e em testes também salve, reabra e leia de novo - Trate
FalsemaisErrorMessagecomo informação: uma rejeição nomeandoNM, um conflito de pai ou um nome duplicado significa que a posse do arquivo é genuinamente ambígua - Se você gera PDFs, emita campos e widgets como objetos indiretos e dê a todo widget um
/NMúnico; isso remove a ambiguidade na fonte - Use
SetFormFieldValuespara preenchimentos de vários campos ou entre páginas para que uma falha tardia não possa deixar metade do formulário atualizado - Nunca rode
Trimou outros helpers RTL destringsobre bytes PDF brutos guardados emAnsiString
Seja claro sobre as fronteiras também. Documentos assinados, formulários XFA, snapshots criptografados e arquivos 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 perguntas de posse ficam ainda mais difíceis; enxertar campos AcroForm entre documentos PDF trata desse caso
O PDFium Component traz essa escrita de formulários tipada e transacional para aplicações Delphi, C++Builder e Lazarus junto com 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