El valor de un campo de formulario PDF "se revierte" cuando el campo está guardado como un diccionario directo: PDFium promueve la copia en el arreglo /Annots de la página a un objeto indirecto, mientras /Fields conserva una segunda copia directa. PDFium Component para Delphi maneja las escrituras tipadas de campos unificando ambas referencias en un solo dueño indirecto antes de escribir
El síntoma suele leerse como una casa embrujada. La escritura devuelve True, y la lectura siguiente le devuelve el valor viejo. Guarde, reabra, y el valor viejo sigue allí. Nada lanzó, y el PDF se ve perfectamente ordinario en cada visor que usted tiene
El síntoma: una escritura exitosa que no se queda
El patrón de fallo es angosto pero muy repetible: solo aparece en formularios cuyos diccionarios de campos están escritos inline (como objetos directos) en vez de como objetos indirectos numerados. La mayoría de las herramientas de autoría emiten campos indirectos, así que el problema se esconde por meses y luego aparece en archivos de un generador en particular, muchas veces un exportador artesanal que escribe el /AcroForm completo inline
Dos observaciones juntas apuntan a la causa: la escritura reporta éxito, y una lectura nativa del campo a través de la capa de formularios devuelve el valor anterior. Cuando vea ambas, deje de sospechar de su encoding de cadenas y empiece a mirar la propiedad de objetos
¿Por qué el valor de un campo PDF se revierte tras una escritura exitosa?
El valor de un campo PDF se revierte porque dos diccionarios separados ambos reclaman ser el mismo campo, y la escritura actualizó la copia que el árbol de campos no consulta. El valor de un campo vive en su entrada /V, e ISO 32000-1 §12.7.3.1 define dónde esa entrada es autoritativa: en el diccionario de campo alcanzado desde /AcroForm /Fields, bajando por /Kids hasta el campo terminal, con las entradas heredables resueltas desde /Parent
La misma cláusula permite una conveniencia que causa todo el problema. Cuando un campo tiene exactamente una anotación widget, el diccionario de campo y el diccionario de la anotación widget pueden fusionarse en un único diccionario. En un archivo bien formado ese diccionario fusionado es un objeto indirecto, referenciado desde dos lugares: desde /Fields (o los /Kids de un padre) y desde el arreglo /Annots de la página. Dos referencias, un objeto, un /V
Dos copias de un diccionario
ISO 32000-1 §7.3.10 es explícito en que solo un objeto indirecto tiene una identidad compartible. Un diccionario directo no tiene número de objeto, así que cuando un generador escribe el diccionario fusionado campo/widget inline tanto en /Fields como en /Annots, el archivo no contiene un objeto referenciado dos veces. Contiene dos diccionarios de aspecto igual que casualmente tienen las mismas claves y valores
PDFium tolera esto, y al hacerlo vuelve permanente la división. Cuando necesita un handle a la anotación, PDFium puede promover el diccionario widget directo en el arreglo /Annots de la página a un objeto indirecto, pero la copia directa equivalente se queda donde estaba dentro de /AcroForm /Fields (o dentro de los /Kids del padre). Desde ese momento el documento tiene dos dueños para un campo lógico. Un writer que edita /V en la anotación de la página obtiene un éxito genuino a nivel de diccionario. El árbol de campos, que es lo que la capa de formularios y cada lector posterior tratan como fuente de verdad, nunca ve el cambio
Por eso el valor obsoleto sobrevive a un guardado y reapertura: el nuevo valor se serializó fielmente en un objeto que la jerarquía de campos no posee
Cómo PDFium Component restablece un único dueño de campo
PDFium Component normaliza la propiedad directa de campos antes de cada escritura tipada: promueve los diccionarios /AcroForm y de campos directos a objetos indirectos, ata cada widget directo a su anotación de página correspondiente, y reescribe /Fields, /Kids, /Parent y /Annots para que cada referencia apunte al mismo objeto indirecto. Solo entonces cambia /V, así que el valor del campo, el widget seleccionado y el documento guardado comparten un dueño
La parte difícil es la identidad. Dos diccionarios directos con contenido idéntico no prueban que son el mismo widget; un formulario puede legítimamente contener dos widgets que se parezcan. El normalizador por tanto se niega a adivinar, y exige un nombre de anotación /NM explícito y único (ISO 32000-1 §12.5.2) en cada widget directo. Las reglas que aplica la ruta de escritura son estrictas:
- Un widget directo necesita un
/NMno vacío de a lo más 1024 unidades UTF-16, y exactamente una anotación de página puede llevar el mismo nombre - La copia directa y la anotación de página deben serializarse idénticamente aparte de
/Parenty el/APregenerado por PDFium; cualquier otra diferencia rechaza la escritura - Ambos enlaces de padre se verifican por separado, así que un
/Parentcorrecto en la anotación de página no puede enmascarar uno conflictivo en la copia directa original - Un
/Parentfaltante se provee solo cuando una propiedad única en el árbol de campos hace la respuesta inequívoca, y las copias directas de padre equivalentes se reatan a su dueño promovido - Nombres de campo totalmente calificados duplicados, árboles de campos cíclicos o con múltiples dueños, y campos de firma se rechazan antes de publicar nada
- Las anotaciones no relacionadas, como enlaces en la misma página, y las entradas
/AcroFormno relacionadas se conservan intactas
El normalizador además corre bajo presupuestos duros: 4096 visitas de árbol de campos, páginas o miembros de arreglos, profundidad de anidamiento 64, 256 objetos indirectos nuevos, 1 MiB por diccionario promovido, 4 MiB de bytes promovidos en total, 128 MiB de bytes de diccionarios inspeccionados, y 4096 unidades UTF-16 por nombre completo de campo con 262144 en todo el formulario. Un archivo hostil o roto golpea un presupuesto y falla con un mensaje de error en vez de convertir un llenado de formulario en una denegación de servicio
La operación entera es transaccional. TPdf.SetFormFieldValues captura una instantánea, construye una actualización incremental que contiene los objetos normalizados y los valores nuevos, carga esa candidata, y solo reemplaza el documento vivo una vez que la candidata verifica. Si la normalización rechaza el archivo, o una petición posterior del lote falla, el documento original, sus valores y su cursor lógico de campos quedan exactamente como estaban
Escribir y verificar un valor de campo directo en Delphi
El patrón seguro es escribir, luego releer a través de la capa nativa de formularios, y tratar una discrepancia como fallo incluso cuando la escritura devolvió True. TPdf.SetFormFieldValue dirige un campo por índice en la página actual, recibe un TPdfFormValue tipado y llena un TPdfFormValueWriteReport; TPdf.FormField[Index] relee el valor por la capa de formularios de PDFium, que resuelve el árbol de campos en vez de confiar en el 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 apunta a la página actual
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 el árbol de campos, no contra nuestra propia variable
if Pdf.FormField[FieldIndex] <> NewText then
raise Exception.CreateFmt('Field "%s" did not keep the new value',
[Pdf.FormFieldInfo[FieldIndex].Name]);
end;
Sobre un archivo con propiedad irresoluble, por ejemplo un widget directo sin /NM, SetFormFieldValue devuelve False, Report.UpdatedWidgets es cero y Report.ErrorMessage nombra el problema, como "Direct widgets require an explicit unique NM identity". Ese es el comportamiento que usted quiere: un rechazo claro en vez de una media escritura silenciosa. Una precondición más vale conocer. La escritura tipada es solo de anotaciones y no adquiere foco nativo, así que si un editor nativo de campos sigue enfocado toda la transacción se rechaza antes de tomar la instantánea; llame Pdf.ClearFormFieldFocus deliberadamente primero en vez de reintentar en un bucle
Para varios campos, especialmente entre páginas, use la forma de lote. TPdf.SetFormFieldValues acepta de 1 a 256 peticiones, aplica todas a una candidata, y publica todo o nada. TPdfFormValuesWriteReport.FailedRequest le dice qué petición rompió el 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 cuenta cada widget sincronizado
end;
Dos peticiones que dirigen el mismo campo terminal se rechazan como dueño duplicado en vez de dejar que la última escritura gane, y el documento fuente se preserva. Los valores checkbox y radio siguen las mismas reglas; la división /V contra /AS para botones está cubierta en valor de campo checkbox contra estado de widget en PDFium
Probar que el valor sobrevive al guardado y reapertura
Una relectura en la misma sesión prueba que el documento vivo es coherente. Un guardado y reapertura prueba que el archivo lo es, y la verificación es suficientemente barata como para mantenerla en sus propias pruebas de integración
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;
Dos trampas más a nivel de bytes: Trim y el /ID del trailer
Normalizar campos directos significa releer bytes crudos de diccionarios, lo que trae a colación dos trampas menores relevantes para cualquier código Delphi que toque sintaxis PDF
La primera es SysUtils.Trim sobre bytes PDF crudos guardados en un AnsiString. Una cadena literal como una opción choice puede legítimamente contener texto UTF-16BE con una marca de orden de bytes FE FF y bytes NUL incrustados. En Delphi Unicode, SysUtils.Trim recibe un string, así que la llamada convierte silenciosamente el AnsiString a UTF-16 usando la página de códigos ANSI activa y convierte el resultado de vuelta. Dependiendo de esa página de códigos, el viaje de ida y vuelta puede reescribir bytes altos, dañar el BOM e incluso perturbar el delimitador que sigue. El enfoque seguro mantiene el lexer sobre bytes: salte el whitespace PDF por posición (ISO 32000-1 §7.2.2 lista NUL, tab, LF, FF, CR y espacio) y corte con Copy, decodificando solo cuando un valor se sabe que es una cadena de texto PDF
// Arriesgado sobre bytes PDF crudos: AnsiString -> string -> AnsiString (W1057/W1058)
Token := Trim(Token);
// Seguro por bytes: salte el whitespace PDF por posición, luego corte
P := 1;
while (P <= Length(Token)) and (Token[P] in [#0, #9, #10, #12, #13, ' ']) do
Inc(P);
Token := Copy(Token, P, MaxInt);
La segunda trampa vive en la actualización incremental misma. ISO 32000-1 §7.5.5 (Tabla 15) y §14.4 exigen que el /ID del trailer sea un arreglo de dos cadenas de bytes. El helper interno que compone el identificador de la actualización dispersa devuelve solo el contenido de las dos cadenas, sin la clave /ID ni los corchetes del arreglo, y el trailer debe envolverlo explícitamente como /ID [ ... ]. El loader indulgente de PDFium, y una re-guardada completa posterior, ambos esconden un wrapper faltante, así que la verificación confiable es parsear los archivos candidato y guardado con un parser estricto independiente y buscar dos cadenas de 16 bytes. Si usted escribe actualizaciones incrementales por su cuenta, la misma trampa aplica; el artículo de actualización incremental para PDFs cifrados cubre la cadena del trailer con más profundidad
¿Qué debería hacer en su propio código de formularios Delphi?
- Releer siempre el valor con
FormField[Index]tras una escritura, y en pruebas además guardar, reabrir y releer - Tratar
FalsemásErrorMessagecomo información: un rechazo que nombraNM, un conflicto de padre o un nombre duplicado significa que la propiedad del archivo es genuinamente ambigua - Si genera PDFs, emita campos y widgets como objetos indirectos y dele a cada widget un
/NMúnico; eso elimina la ambigüedad en la fuente - Usar
SetFormFieldValuespara llenados multi-campo o entre páginas para que un fallo tardío no pueda dejar media forma actualizada - Nunca correr
Trimni otros helpersstringde la RTL sobre bytes PDF crudos guardados enAnsiString
Sea claro también con las fronteras. Los documentos firmados, los formularios XFA, las instantáneas cifradas y los archivos cuya propiedad de widgets no puede resolverse de forma única quedan fuera del perfil declarado y se rechazan en vez de forzarse. Si su flujo mueve campos entre documentos, las preguntas de propiedad se ponen más difíciles aún; el injerto de campos AcroForm entre documentos PDF trata ese caso
El PDFium Component lleva esta escritura de formularios tipada y transaccional a aplicaciones Delphi, C++Builder y Lazarus junto al renderizado, la extracción de texto y la edición de documentos. Historial de versiones, licenciamiento y descargas están en la página del producto PDFium Component para Delphi