Un formulario de encuesta muestra dos listas desplegables en Acrobat, group.a y group.b. Recorra /AcroForm /Fields con código que trate cualquier nodo que lleve su propio /FT como un campo terminado, y el recuento vuelve como 1: un único campo llamado group con dos widgets. Pida group.b por nombre y la búsqueda falla. Abra el diccionario del hijo y no hay ningún /FT en él, ni /Ff, ni lista de opciones. Nada está roto. El tipo, los flags y las opciones están un nivel más arriba en el padre, y la especificación PDF permite explícitamente que vivan allí
La estructura es legal y económica. Una herramienta de autoría escribe un nodo grupo con /FT /Ch, /Ff 131078 y un array /Opt una sola vez, y cuelga debajo dos hijos con nombre, cada uno un diccionario fusionado de campo más widget que solo lleva /T, /Parent, /Rect y su propio /V. Leer correctamente semejante formulario requiere tres cosas: seguir /Parent para cada clave heredable, decidir dónde termina de verdad el árbol de campos, y asegurarse de que un reset escribe un valor del tipo PDF correcto. Esta pieza recoge donde fijar valores de campos de formulario en un PDF cargado con Delphi se detiene
¿Qué entradas AcroForm puede heredar un campo de su padre?
ISO 32000-1 §12.7.3.1 (Tabla 220) marca /FT, /Ff, /V y /DV como heredables, y la Tabla 229 de §12.7.4.3 hace lo mismo con el /MaxLen de un campo de texto. Los arrays de opciones reciben aquí el mismo tratamiento, porque los productores que comparten una lista de elección entre hijos guardan /Opt en el grupo. Un lector que solo mire el diccionario local reportará por tanto el tipo equivocado, flags de 0 y un valor vacío para un hijo perfectamente válido
HotPDF Delphi Component responde cada una de esas lecturas de la misma manera. Busca la clave en el diccionario propio del campo y, si la clave está allí, resuelve la referencia indirecta y se detiene. Si no, sigue /Parent y prueba de nuevo, hasta 128 niveles como máximo. Ese tope no es decoración. Los punteros /Parent en archivos dañados pueden formar un bucle que no tiene nada que ver con /Kids, y un lector que confíe en ellos sin condiciones se cuelga con el primer archivo así que encuentra
Una regla de ese recorrido es fácil de entender al revés: gana el primer diccionario que contiene la clave, incluso cuando el valor guardado allí es una cadena vacía. Un /V () local en group.b es el autor diciendo "este hijo está en blanco", y el recorrido se detiene allí en lugar de seguir subiendo hacia un valor que el padre pueda sostener. Tratar el vacío como "ausente" haría que un hijo limado mostrara la selección del padre, que es justo lo contrario de lo que el autor escribió
Los getters públicos se apoyan todos en esa búsqueda: GetFormFieldType, GetFormFieldValue, GetLoadedFormFieldFlags, IsFormFieldRequired, IsFormFieldNoExport, GetLoadedFormFieldMaxLength, GetLoadedFormFieldDefaultValue, y los ayudantes de opciones GetLoadedFormFieldOptionCount y GetLoadedFormFieldOptions. Así que leer el hijo del grupo de la encuesta no necesita código especial:
var
Pdf: THotPDF;
Field: THPDFLoadedFormField;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('survey.pdf') <= 0 then Exit;
// 'group' lleva /FT /Ch, /Ff 131078 y /Opt; el hijo
// 'group.b' lleva solo /T, /Parent, /Rect y su propio /V
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)); // el /V local
end;
finally
Field.Free;
end;
finally
Pdf.Free;
end;
end;
GetFormField devuelve un nuevo envoltorio THPDFLoadedFormField cada vez y lanza una excepción cuando el nombre es desconocido: deje la llamada fuera del bloque try del envoltorio y libere usted el envoltorio
Encontrar dónde termina el árbol de campos
Un /FT local es el test equivocado para un campo terminal, porque un padre puede suministrar el tipo y aun así poseer campos hijos con nombre. El recorrido tentador deja de descender en cuanto un nodo tiene su propio /FT o no tiene /Kids. En el formulario de encuesta, group tiene a la vez /FT /Ch y /Kids, así que ese recorrido lo registra como un campo con dos anotaciones widget. Los nombres group.a y group.b desaparecen. El recuento de campos es 1. Una búsqueda por nombre de hijo falla, y una escritura de valor solo puede caer en el padre compartido
HotPDF mira en cambio a los kids. Un kid cuenta como campo hijo si tiene su propio /T, tiene su propio /Kids, o no es un diccionario /Subtype /Widget. Solo cuando ningún kid cualifica es el nodo terminal, y sus kids se tratan entonces como sus widgets. Los dos casos límite detrás de esa regla vienen de diccionarios fusionados, que §12.7.3.1 permite cuando un campo tiene exactamente un widget:
- Un diccionario fusionado con nombre lleva
/Subtype /Widgety aun así es un campo hijo. El subtype por sí solo no puede empujarlo a la lista anónima de widgets del padre; el/Tdecide - Algunos productores repiten el
/FTdel padre en cada widget anónimo. Así que un/FTen un widget tampoco puede leerse como "aquí empieza un campo nuevo"
La misma clasificación dirige el índice de campos, FormFieldExists y RemoveFormField. Cada uno de esos recorridos registra además los diccionarios que ya ha visitado y se rinde pasado el nivel 128. Suponga que el grupo es el objeto 5 y se lista dos veces entre sus propios kids: /Kids [5 0 R 5 0 R 6 0 R 7 0 R]. Sigue reportando exactamente dos campos, los hijos 6 y 7. Sin recursión infinita, y sin un tercer campo fantasma por la autorreferencia
¿Cómo evita RemoveFormField borrar campos hermanos?
RemoveFormField borra solo el hijo que usted nombra porque descubrimiento y borrado comparten una misma definición de campo terminal. El overload por nombre resuelve un índice de campo a partir de la lista de campos, y después recorre /AcroForm /Fields una segunda vez contando campos terminales para encontrar el diccionario que borrar. Si el segundo recorrido usara la regla tentadora del /FT mientras que el primero usara la regla basada en kids, los índices discreparían: el recorrido de borrado vería group como el campo 0, y eliminar group.a se llevaría por delante al padre, al hermano y a los dos widgets
Así que el recorrido de borrado aplica el mismo test de campo hijo y el mismo conjunto de visitados que el descubrimiento. Recoge las anotaciones widget que pertenecen al hijo eliminado, retira solo esas del /Annots de cada página, y elimina el padre solo cuando su array /Kids acaba vacío. Con group.b todavía listado, el padre se queda, y con él el tipo, los flags y las opciones que group.b hereda
// Eliminar un hijo con nombre; su hermano y el padre compartido sobreviven
Pdf.RemoveFormField('group.a');
// Comprobaciones de cordura que añadiría usted, no forman parte de la biblioteca
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipo, flags y opciones se siguen resolviendo a través del padre
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Cuando compruebe una eliminación en sus propios formularios, mire en tres sitios, porque cada uno caza un error distinto: el /Kids del padre, el /Annots de la página, y el valor y la apariencia del hermano que sobrevive. Hágalo una vez tras SaveLoadedDocument y otra tras SaveIncrementalUpdate, ya que un guardado incremental solo anexa los objetos cambiados y el /Kids del padre tiene que estar entre ellos
Qué copia ResetLoadedFormField de un /DV heredado
ResetLoadedFormField escribe un /V local que es una copia fresca del /DV heredado, con el mismo tipo de objeto PDF, y valida el default completo antes de tocar el campo. El default de una casilla es un nombre como /Yes. El default de un list box multiselección es un array de cadenas. Un default de texto puede ser una cadena UTF-16 hexadecimal como <FEFF...>. Pase cualquiera de esos por GetLoadedFormFieldDefaultValue y SetFormFieldValue y obtiene una cadena donde va un nombre, una cadena vacía donde había un array, o dígitos hex literales
El reset ramifica por tanto según lo que el /DV heredado es de verdad:
- Campos de texto y de elección con un default de cadena reciben un objeto cadena nuevo que conserva el flag hexadecimal del original
- Campos de elección con un default de array reciben un array nuevo de cadenas nuevas; un array que contenga cualquier cosa que no sean cadenas lanza una excepción
- Casillas y botones de radio reciben un objeto nombre nuevo
- Un default de cualquier otro tipo lanza una excepción, y
/Vy/Ise quedan exactamente como estaban
Copiar en lugar de referenciar es deliberado, y es la única opción segura. Un /V hijo que apuntara al array /DV del padre compartiría un objeto entre "valor actual" y "valor por defecto"; el siguiente SetFormFieldValue sobre ese hijo reescribiría en silencio el default de cada hermano. Unos bytes extra por campo reseteado es un precio barato por mantener los dos separados. Los pushbuttons, que no tienen valor (Tabla 226, bit 17), y los campos de firma se quedan en un camino llano que escribe una copia de cadena de su propio /DV
Cuando no existe ningún /DV en toda la cadena hacia arriba, el método escribe una cadena vacía local, o /Off para un campo de casilla o de radio. Borrar el /V del hijo parecería más ordenado y daría la respuesta equivocada. Si el padre sostiene un valor actual, eliminar la anulación del hijo trae ese valor de vuelta directamente, y el campo "reseteado" muestra ahora algo que el usuario nunca escribió en él
var
Field: THPDFLoadedFormField;
begin
// El padre 'palette' es un list box MultiSelect con
// /Opt [(b) (g) (r)] y /DV [(b) (r)]; el hijo solo lleva /T y /V
Field := Pdf.GetFormField('palette.primary');
try
try
Pdf.ResetLoadedFormField(Field.Index);
// El hijo tiene ahora su propio /V [(b) (r)] y /I [0 2];
// el array /DV del padre queda intacto
except
on E: Exception do
// Default mal formado (tipo equivocado, elemento de array no cadena):
// /V e /I se quedaron como estaban
Writeln('Reset skipped: ', E.Message);
end;
// El getter escalar no puede representar un default de array
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // vacío
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Un reset de un solo campo es también algo distinto de la acción ResetForm de §12.7.5.3, que un visor ejecuta sobre un conjunto de campos cuando el usuario pulsa un botón; esa acción se cubre en construir campos y acciones AcroForm con HotPDF. ResetLoadedFormField edita un campo cargado, sigue su propia regla para el caso sin default, y marca el campo como cambiado para que el recálculo incremental lo recoja
Mantener /V, /I y /AS en acuerdo
Un reset solo es correcto si el índice de selección y el estado de apariencia siguen al valor, y ResetLoadedFormField termina con los mismos dos pasos de seguimiento que SetFormFieldValue. Para los campos de elección, borra el /I local del hijo, coteja cada valor contra la mitad de export de cada entrada /Opt, y escribe un /I nuevo y ordenado. Resetear a [(b) (r)] contra opciones b, g, r da /I [0 2]. El /I heredado del padre, si lo hay, queda intacto
Los botones trabajan a partir del tipo heredado. Una casilla hija cuyo /FT /Btn vive en el padre recibe también su /AS actualizado. En el lado de escritura, SetFormFieldValue y SetLoadedFormFieldDefaultValue guardan un objeto nombre para un botón heredado que no es pushbutton incluso cuando el hijo no tiene ninguna entrada local de donde copiar el tipo. Y cuando EnsureLoadedFieldAppearanceStream reconstruye apariencias de botones, escribe /AS /Off salvo que el valor coincida con el estado activado, y da a cada flujo de estado /Type /XObject, /Subtype /Form y un /BBox. Sin esa comprobación del /Off, regenerar apariencias tras un reset volvería a mostrar la casilla marcada antes incluso de guardar el archivo
Getters escalares y otros bordes con los que planear
Los getters de cadena se quedan escalares, y el código que compare su salida debería saber qué aplanan. GetFormFieldValue y GetLoadedFormFieldDefaultValue devuelven una cadena vacía para un valor de array, convierten números y booleanos en 42 o true, y reportan una cadena codificada en hex en su grafía hexadecimal. Si necesita saber si el default de un list box guarda dos selecciones o ninguna, el getter de default no puede decírselo: ambas vuelven como cadena vacía
Un ciclo de /Parent termina la búsqueda sin lanzar, así que un campo cuyo tipo se pierde dentro de un ciclo reporta lfftUnknown y flags de 0. Trate un lfftUnknown en un campo con nombre como señal de un archivo dañado que merece registrarse. SetFormFieldValue y ResetLoadedFormField escriben siempre el hijo al que usted apunta y nunca promueven un valor al padre compartido. Eso es lo correcto para hijos independientes como las dos listas desplegables de la encuesta. Los grupos de radio son distintos: la selección pertenece al campo que posee el grupo, así que diríjase a ese campo y no a uno de sus kids
Cada llamada compromete además un campo por su cuenta. Un bucle de resets sobre veinte campos son veinte ediciones independientes; si la quinta lanza excepción por un default mal formado, las cuatro primeras ya están aplicadas. Para trabajo por lotes, lea primero el tipo y el default de cada objetivo, decida qué campos va a tocar, y solo entonces empiece a escribir
La búsqueda de atributos heredados, la clasificación del árbol de campos basada en kids y el reset tipado pertenecen todos a la API de formularios cargados del HotPDF Delphi Component, que cubre también los proyectos Delphi y C++Builder que necesitan añadir campos nuevos a un documento existente, como se muestra en añadir campos AcroForm a un PDF cargado en Delphi