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 cargue su propio /FT como un campo terminado, y el conteo regresa como 1: un solo 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 se sientan un nivel arriba en el padre, y la especificación PDF permite explícitamente que vivan ahí
La disposición es legal y económica. Una herramienta de autor escribe un nodo de grupo con /FT /Ch, /Ff 131078 y un arreglo /Opt una sola vez, y luego cuelga debajo dos hijos con nombre, cada uno un diccionario fusionado de campo más widget que guarda solo /T, /Parent, /Rect y su propio /V. Leer semejante formulario correctamente toma tres cosas: seguir /Parent para cada clave heredable, decidir dónde termina realmente el árbol de campos, y asegurarse de que un reset escribe un valor del tipo PDF correcto. Esta pieza retoma donde fijar valores de campos de formulario en un PDF cargado con Delphi se detuvo
¿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 en §12.7.4.3 hace lo mismo con el /MaxLen de un campo de texto. Los arreglos 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 revisa el diccionario local reportará entonces 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 forma. Busca la clave en el diccionario propio del campo y, si la clave está ahí, resuelve la referencia indirecta y se detiene. Si no, sigue /Parent y reintenta, hasta por 128 niveles. Ese tope no es decoración. Los punteros /Parent en archivos dañados pueden formar un ciclo que no tiene nada que ver con /Kids, y un lector que confíe en ellos incondicionalmente se cuelga con el primer archivo semejante que encuentra
Una regla de ese recorrido es fácil de tomar al revés: el primer diccionario que contiene la clave gana, aunque el valor guardado ahí sea un string vacío. Un /V () local en group.b es el autor diciendo "este hijo está en blanco", y el recorrido se detiene ahí en lugar de seguir subiendo hacia un valor que el padre quizá sostenga. Tratar lo vacío como "ausente" haría que un hijo limpiado muestre la selección de su padre, que es 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 helpers 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 la prueba equivocada 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 apenas un nodo tiene su propio /FT o no tiene /Kids. En el formulario de encuesta, group tiene ambos /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 conteo de campos es 1. Una búsqueda por nombre de hijo falla, y una escritura de valor solo puede aterrizar en el padre compartido
HotPDF mira a los kids en su lugar. 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 califica el nodo es terminal, y sus kids se tratan entonces como sus widgets. Ambos casos borde 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 sigue siendo un campo hijo. El subtype por sí solo no puede empujarlo a la lista de widgets anónimos 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 maneja el índice de campos, FormFieldExists y RemoveFormField. Cada uno de esos recorridos también registra los diccionarios que ya visitó 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]. Aun así reporta 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 eliminar campos hermanos?
RemoveFormField elimina solo el hijo que usted nombra porque descubrimiento y eliminación comparten una definición de campo terminal. El overload por nombre resuelve un índice de campo desde la lista de campos, luego recorre /AcroForm /Fields una segunda vez contando campos terminales para encontrar el diccionario a eliminar. Si el segundo recorrido usara la tentadora regla /FT mientras el primero usó la regla basada en kids, los índices discreparían: el recorrido de eliminación vería group como campo 0, y eliminar group.a se llevaría al padre, al hermano y a ambos widgets
Así que el recorrido de eliminación aplica la misma prueba de campo hijo y el mismo conjunto de visitados que el descubrimiento. Colecciona las anotaciones widget que pertenecen al hijo eliminado, quita solo esas de los /Annots de cada página, y elimina el padre solo cuando su arreglo /Kids termina vacío. Con group.b todavía listado, el padre se queda, y el tipo, los flags y las opciones que group.b hereda se quedan con él
// Elimina un hijo con nombre; su hermano y el padre compartido sobreviven
Pdf.RemoveFormField('group.a');
// Chequeos de cordura que usted agregaría, no parte de la librería
Assert(Pdf.GetFormFieldCount = 1);
Assert(Pdf.FormFieldExists('group.b'));
// Tipo, flags y opciones siguen resolviéndose a través del padre
Assert(Pdf.GetFormFieldType('group.b') = lfftChoice);
Pdf.SaveLoadedDocument('survey-trimmed.pdf');
Cuando revise una eliminación en sus propios formularios, mire en tres lugares, porque cada uno atrapa un error distinto: los /Kids del padre, los /Annots de la página, y el valor y la apariencia del hermano sobreviviente. Hágalo una vez después de SaveLoadedDocument y otra después de SaveIncrementalUpdate, ya que un guardado incremental solo anexa los objetos cambiados y los /Kids del padre deben 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 un checkbox es un nombre como /Yes. El default de un list box multiselección es un arreglo de strings. Un default de texto puede ser un string UTF-16 hexadecimal como <FEFF...>. Pase cualquiera de esos por GetLoadedFormFieldDefaultValue y SetFormFieldValue y obtiene un string donde va un nombre, un string vacío donde había un arreglo, o dígitos hex literales
El reset entonces bifurca según lo que el /DV heredado realmente es:
- Los campos de texto y de elección con default de string reciben un objeto string nuevo que conserva el flag hexadecimal del original
- Los campos de elección con default de arreglo reciben un arreglo nuevo de strings nuevos; un arreglo que contenga algo que no sean strings lanza una excepción
- Los checkboxes y los radio buttons reciben un objeto de nombre nuevo
- Un default de cualquier otro tipo lanza una excepción, y
/Ve/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 arreglo /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 cuantos bytes extra por campo de reset es un precio barato por mantenerlos separados. Los pushbuttons, que no tienen valor (Tabla 226, bit 17), y los campos de firma se quedan en un camino plano que escribe una copia string de su propio /DV
Cuando no existe ningún /DV en toda la cadena, el método escribe un string vacío local, o /Off para un campo checkbox o radio. Borrar el /V del hijo se vería más ordenado y daría la respuesta equivocada. Si el padre sostiene un valor actual, quitar el override del hijo trae ese valor de vuelta directo, y el campo "reseteado" ahora muestra algo que el usuario nunca tecleó 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 guarda solo /T y /V
Field := Pdf.GetFormField('palette.primary');
try
try
Pdf.ResetLoadedFormField(Field.Index);
// El hijo ahora tiene su propio /V [(b) (r)] y /I [0 2];
// el arreglo /DV del padre queda intacto
except
on E: Exception do
// Default malformado (tipo equivocado, ítem de arreglo que no es string):
// /V e /I quedaron como estaban
Writeln('Reset skipped: ', E.Message);
end;
// El getter escalar no puede representar un default de arreglo
Writeln(Pdf.GetLoadedFormFieldDefaultValue(Field.Index)); // vacío
finally
Field.Free;
end;
Pdf.SaveLoadedDocument('survey-reset.pdf');
end;
Un reset de un solo campo también es cosa distinta de la acción ResetForm de §12.7.5.3, que un visor corre sobre un conjunto de campos cuando el usuario hace clic en un botón; esa acción está cubierta 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 levante
Mantener /V, /I y /AS de 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 campos de elección, quita el /I local del hijo, empareja cada valor contra la mitad de exportación de cada entrada /Opt, y escribe un /I nuevo ordenado. Resetear a [(b) (r)] contra opciones b, g, r da /I [0 2]. El /I heredado del padre, si existe, queda intacto
Los botones trabajan desde el tipo heredado. Un checkbox hijo cuyo /FT /Btn vive en el padre igual recibe su /AS actualizado. Del lado de la escritura, SetFormFieldValue y SetLoadedFormFieldDefaultValue guardan un objeto de nombre para un botón heredado que no es pushbutton aunque el hijo no tenga entrada local de donde copiar el tipo. Y cuando EnsureLoadedFieldAppearanceStream reconstruye las apariencias de botones, escribe /AS /Off salvo que el valor coincida con el estado encendido, y le da a cada stream de estado /Type /XObject, /Subtype /Form y un /BBox. Sin esa comprobación de /Off, regenerar las apariencias después de un reset mostraría la casilla marcada otra vez antes de que el archivo siquiera se guarde
Getters escalares y otros bordes que conviene planificar
Los getters de string se quedan escalares, y el código que compara su salida debería saber qué aplastan. GetFormFieldValue y GetLoadedFormFieldDefaultValue devuelven un string vacío para un valor de arreglo, vuelven números y booleanos 42 o true, y reportan un string codificado en hex en su grafía hexadecimal. Si usted necesita saber si el default de un list box guarda dos selecciones o ninguna, el getter de defaults no puede decírselo: ambos regresan como string vacío
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 vale registrarse. SetFormFieldValue y ResetLoadedFormField siempre escriben el hijo que usted direcciona y jamás promueven un valor al padre compartido. Eso es 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 direcciona ese campo en lugar de uno de sus kids
Cada llamada también confirma un campo por su cuenta. Un bucle de resets sobre veinte campos son veinte ediciones independientes; si la quinta lanza sobre un default malformado, las primeras cuatro 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 también cubre proyectos Delphi y C++Builder que necesitan agregar campos nuevos a un documento existente, como se muestra en agregar campos AcroForm a un PDF cargado en Delphi