Un valor de exportación de list box formado por Deep, un salto de línea y Blue es PDF perfectamente legal, y también lo es una segunda opción llamada Red. Elija ambas, aplane la selección en un solo string separado por LF para un archivo de intercambio, y el otro lado recibe Deep\nBlue\nRed: dos selecciones o tres, y nada en los bytes dice cuál. Los valores multiselección vuelven intactos de FDF o XFDF solo cuando se quedan como arrays todo el camino, desde el momento en que salen del PDF hasta que se escriben de vuelta en él
¿Por qué una unión con salto de línea nunca puede cargar un valor multiselección?
Un string unido pierde los límites entre valores, y como un valor puede contener el separador, ningún importador puede reconstruir esos límites. ISO 32000-1 §12.7.4.4 deja que la entrada /V de un campo de elección contenga un único string de texto o un array de strings de texto; un list box con el flag MultiSelect (bit 22 de /Ff) usa la forma de array apenas se elige más de una opción. La misma sección define /I, un array de índices de opción base cero en orden ascendente, que es como un visor distingue dos opciones que comparten un valor de exportación. Ninguna de las dos estructuras cabe por una API de tipo string. GetFormFieldValue en HotPDF devuelve un AnsiString y lee la forma de string de /V, así que cualquier exportación construida sobre él solo puede escribir un escalar por campo
El lado de la importación tiene el problema espejo. XFDF escribe cada selección como su propio elemento <value>, y un importador que se los entrega a un setter escalar primero tiene que pegarlos, normalmente con LF. Con la opción Deep/Blue de arriba, ese pegamento es indistinguible de los datos. HotPDF por eso mantiene a los escalares fuera del medio de la ida y vuelta: ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF y ExportLoadedFormToXFDF leen el array directo del campo, y los métodos de importación correspondientes construyen un array nuevo sin pasar jamás por un string unido
¿Qué contienen los archivos FDF y XFDF exportados?
En disco los límites quedan visibles. En FDF un campo multiselección se convierte en un array tipado, y cada ítem conserva la grafía que tenía en el PDF de origen: los strings hexadecimales salen como hex, y los strings literales salen como literales con CR y LF escapados como \r y \n. En el archivo que escribe ExportLoadedFormToFDF, un /RV de texto enriquecido o un /Ff local distinto de cero en el campo viaja en el mismo diccionario de campo. En XFDF el elemento raíz declara xml:space="preserve", que ISO 19444-1 exige y que convierte cada carácter de espacio en blanco dentro de un elemento de texto en dato. Eso es una trampa para el pretty-printing. HotPDF escribe el tag de apertura, el texto escapado y el tag de cierre de cada <value> en una sola línea, deja la indentación fuera del elemento, y codifica CR, LF y TAB como referencias de carácter para que un parser XML que aplique normalización de fines de línea no pueda reescribir un par CR LF como un LF solitario
<!-- FDF: un array tipado por campo -->
<< /T (options) /V [(Deep\nBlue) (Red)] >>
<< /T (region) /V [<45553132>] >>
<!-- XFDF: un <value> por selección -->
<xfdf xmlns="http://ns.adobe.com/xfdf/" xml:space="preserve">
<fields>
<field name="options">
<value>Deep
Blue</value>
<value>Red</value>
</field>
</fields>
</xfdf>
Dos casos borde de la exportación importan cuando usted escribe el código que llama. ExportLoadedFormToFDF arma el body FDF completo en memoria y solo entonces crea el archivo de destino, así que un valor que no se puede exportar (un array que contiene algo que no sean strings, por ejemplo) lanza excepción mientras el .fdf anterior en disco sigue intacto. El segundo caso es una carencia genuina del propio XFDF: una selección vacía en un list box que además ofrece un valor de exportación de string vacío. <value/> podría significar "nada seleccionado" u "opción vacía seleccionada", y el formato no tiene una tercera grafía. ExportLoadedFormToXFDF lanza excepción en esa situación, antes de abrir el archivo de destino. FDF no tiene tal problema, porque /V [] y /V [()] son objetos distintos. Los tres exportadores se saltan los terminales de solo widget que no tienen nombre /T, porque ningún importador podría jamás casar semejante entrada con un campo
var
Pdf: THotPDF;
Written: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
begin
// Los list box multiselección se escriben como /V [(...) (...)]
Written := Pdf.ExportLoadedFormToFDF('order-form.fdf');
try
Pdf.ExportLoadedFormToXFDF('order-form.xfdf');
except
on E: Exception do
// Selección vacía más una opción de exportación vacía: XFDF no puede
// distinguirlas, y el archivo .xfdf existente queda intacto
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
¿Cómo se chequea una selección importada contra /Opt?
HotPDF acepta un array entrante solo cuando el objetivo es un campo de elección con el flag MultiSelect activo y cada valor coincide con un valor de exportación del array /Opt del campo (para un par de opción de dos elementos, el primero). Cada slot de opción se puede usar una vez. Una lista con dos opciones que ambas exportan b acepta entonces [<62> <62>] como dos selecciones distintas y rechaza un tercer b, porque no queda un slot sin usar para él. El /I reconstruido sigue el orden de /Opt sin importar el orden en que llegaron los valores, lo que mantiene los índices ascendentes como exige §12.7.4.4. El nuevo /V y el nuevo /I se construyen como objetos sueltos y se asignan solo después de que cada valor pasó, así que un valor rechazado no puede dejar medio array ni índices viejos en el campo
Después de la asignación el campo recibe un appearance stream fresco. Ese paso es fácil de olvidar en un importador escrito a mano, y el síntoma confunde: los visores dibujan el /AP almacenado, no el /V, así que la selección vieja se queda en pantalla y en la impresión aunque los datos cambiaron. El list box también entra al conjunto de campos cambiados, lo que significa que una llamada sin parámetros a RecalculateLoadedFormFieldsIncremental vuelve a correr exactamente los cálculos que lo leen. Las grafías hex que llegan de FDF se quedan hex durante el guardado. Si solo necesita fijar un valor escalar, fijar el valor de un campo de formulario en un PDF cargado cubre ese camino, que por diseño no se ocupa de selecciones múltiples
Un patrón de entrada merece manejo especial. Un PDF puede guardar un valor de exportación ASCII plano como string hex sin marca de orden de bytes, <416272> para Abr, y un exportador que copia el cuerpo del string PDF verbatim produce XFDF cuyo texto de elemento son los dígitos crudos 416272 en lugar del texto decodificado. Un XFDF que lleva un valor de exportación codificado en hex como sus dígitos crudos no puede casar con /Opt por grafía: una comparación estricta no encuentra ninguna opción llamada 416272 y la importación falla. Cuando el importador XFDF de HotPDF recibe semejante archivo, reintenta una vez el valor sin coincidencia decodificándolo como payload hex. El reintento es deliberadamente angosto. Corre solo para un valor sin ninguna coincidencia exacta; cada coincidencia exacta se reserva primero, así que una grafía rescatada nunca puede tomar un slot de opción que un valor posterior, escrito exacto, necesite; y el ítem /V almacenado es el string PDF de la propia opción, así que /V e /I describen la misma selección. Esa es la dosis correcta de lenidad: se dispara solo donde la importación de otro modo habría lanzado excepción, y un valor que ya coincidió nunca se reinterpreta. El emparejamiento mismo pasa por un decodificador Unicode único que entiende PDFDocEncoding, UTF-16 con cualquier marca de orden de bytes y UTF-8, así que el mismo texto lógico coincide con la misma opción incluso en documentos que mezclan codificaciones
Dos trampas del parseo FDF: terminadores hex y números de objeto ajenos
La primera trampa es léxica. En << /T (region) /V <416273>>> el primer > cierra el string hex y los dos siguientes cierran el diccionario. Un scanner que solo busca pares >> termina el diccionario un carácter antes y pierde el campo. El importador FDF a nivel de archivo de HotPDF sigue si está dentro de un string hex o de un literal mientras rebana el array /Fields, y el lector basado en stream detrás de ImportLoadedInterchangeFromFDF tokeniza el archivo completo como sintaxis PDF, así que ambos ven tres cierres donde un escaneo ingenuo ve dos
La segunda trampa es sobre referencias. Un archivo FDF es un pequeño documento de sintaxis PDF con su propia numeración de objetos (ISO 32000-1 §12.7.7), así que /V [11 0 R] significa el objeto 11 del archivo FDF, nunca el objeto 11 del PDF que se está llenando. Resolverlo contra el documento de destino traería silenciosamente el objeto que cargue ese número ahí. En builds de Delphi 2009 en adelante, el lector de intercambio decodifica el FDF como un grafo de objetos completo, sigue la referencia dentro de ese grafo, valida los strings resultantes como cualquier array inline, y lanza excepción si el FDF no contiene el objeto 11. El ImportLoadedFormFromFDF a nivel de archivo lee cada diccionario de campo por su cuenta sin la tabla de objetos del FDF; un array de referencias falla su chequeo de solo strings, y ese campo se salta
Elegir una ruta de importación: saltar, lanzar o revertir
Las tres rutas de importación validan los valores multiselección de la misma forma. Difieren en qué pasa cuando un valor falla. ImportLoadedFormFromFDF se salta cualquier campo que falle y devuelve el número de campos que sí aplicó, así que un conteo menor que el número de entradas que usted envió es la única señal de problema. ImportLoadedFormFromXFDF y ImportLoadedInterchangeFromFDF lanzan excepción en el primer campo rechazado, y cada llamada corre como una transacción sobre el documento cargado: cuando la excepción sale del método, todo campo (y, para la ruta de intercambio, cada anotación) que la llamada ya hubiera cambiado se revierte. Una importación de intercambio que devuelve False también se revierte, con Status y el registro de información limpiados
Para datos que una persona revisó y mandó de vuelta, use una de las rutas que lanzan excepción. Un formulario de pedido aplicado a medias es peor que uno rechazado, porque nada en el PDF guardado muestra qué list boxes vinieron del archivo revisado y cuáles conservaron sus valores viejos. El comportamiento de saltar y contar de ImportLoadedFormFromFDF encaja en un trabajo distinto: un prellenado de mejor esfuerzo desde un FDF que tal vez fue generado para una revisión levemente distinta del formulario
var
Pdf: THotPDF;
Source: TMemoryStream;
Status: AnsiString;
Info: THPDFFDFInterchangeInfo;
begin
Pdf := THotPDF.Create(nil);
Source := TMemoryStream.Create;
try
Source.LoadFromFile('order-form-reviewed.fdf');
if Pdf.LoadFromFile('order-form.pdf', '') > 0 then
try
// Solo campos. Un valor fuera de /Opt, o varios valores apuntados a un
// list de selección única, lanza y revierte toda la importación
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
begin
// Vuelve a correr solo los cálculos que leen los campos importados
Pdf.RecalculateLoadedFormFieldsIncremental;
Pdf.SaveLoadedDocument('order-form-filled.pdf');
end;
except
on E: Exception do
ShowMessage('Import rejected, document unchanged: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Conectar su propio almacén de datos a los callbacks de array de XFDF
La unidad HPDFXFDF, que convierte entre XFDF y cualquier almacén de campos que usted pueda describir con callbacks, lleva el soporte de arrays en un registro separado, THPDFXFDFArrayAccess, y en overloads extra de HPDFXFDFExportFields y HPDFXFDFImportFields. Agregar slots nuevos al registro THPDFXFDFAccess existente habría sido más corto y equivocado. Los llamadores suelen declarar ese registro como variable local y llenar solo los slots que conocen, así que un puntero a función nuevo al final contendría basura del stack que la librería confundiría con un callback. Con el registro separado, el código existente conserva su layout y sus overloads, y esos overloads pasan internamente un registro de array todo-nil
Los dos overloads de importación se comportan distinto con elementos <value> repetidos. El overload escalar original los sigue uniendo con LF y llamando a SetFormFieldValue, porque los llamadores existentes dependen de esa convención. El overload consciente de arrays nunca une: le entrega la lista a SetFormFieldValueArray, cae al setter escalar solo para un valor único que su callback rechace, y lanza excepción cuando su callback rechaza una lista de dos o más. Al exportar, parta de Default(THPDFXFDFArrayAccess) para que los slots que no asigne (incluyendo el opcional SetFormFieldRV) se queden nil. Devuelva True desde GetFormFieldValueArray para cualquier campo con valor de lista, incluido uno sin nada seleccionado, y False para dejar que el callback escalar se encargue del campo. Un array vacío sale como un único <value/> vacío, así que un almacén que ofrece una opción de string vacío debe rechazar ese caso dentro del callback, de la misma manera en que lo hace ExportLoadedFormToXFDF
uses HPDFXFDF;
// Puntero de función plano, no "of object": Context lleva su propio almacén.
// TFormStore y sus métodos son su código, no parte de HotPDF
function StoreGetSelections(Context: Pointer; FieldIndex: Integer;
out Values: THPDFXFDFValueArray): Boolean;
begin
Result := TFormStore(Context).IsListField(FieldIndex);
if Result then
Values := TFormStore(Context).Selections(FieldIndex);
end;
procedure ExportStore(Store: TFormStore; out Bytes: TBytes);
var
Access: THPDFXFDFAccess;
ArrayAccess: THPDFXFDFArrayAccess;
begin
Access := MakeStoreAccess(Store); // sus enlaces escalares existentes
ArrayAccess := Default(THPDFXFDFArrayAccess); // cada slot sin usar es nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
Todo lo anterior asume list boxes que ya existen y ya tienen el bit MultiSelect activo en /Ff. Crear campos de elección y activar sus bits de flag está cubierto en agregar ListBox y otros campos AcroForm a un PDF cargado, y el markup de comentarios que viaja por la parte <annots> de XFDF está cubierto en importación y exportación de anotaciones XFDF en HotPDF. La referencia de métodos de cada llamada mostrada aquí, más una build de prueba para correrlas contra sus propios formularios, está en la página del componente HotPDF Delphi PDF