HotPDF hace round trip de los valores multiselección de un list box por FDF y XFDF manteniendo el valor del campo como array de principio a fin. Desde la versión 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF y ExportLoadedFormToXFDF escriben cada opción seleccionada como su propia cadena FDF o elemento <value> de XFDF, y los métodos de import correspondientes cotejan cada valor contra las opciones del campo y reconstruyen los índices de selección /I antes de cambiar nada. Nada se pega en una sola cadena por el camino
El fallo que esto arregla es fácil de reproducir. Tome un formulario de pedido con un list box multiselección de opciones de producto, deje que un usuario escoja dos, exporte los datos del formulario para un sistema de back-office, y luego importe el archivo editado de vuelta al PDF. Antes de este cambio el list box volvía vacío o equivocado. La razón es que uno de los valores de export contenía un salto de línea, y el viejo camino había aplanado las selecciones en una sola cadena separada por líneas. Sacar varias selecciones de esa cadena nunca fue fiable, y con un valor de export que en sí contiene un salto de línea no puede funcionar en absoluto
¿Por qué unir valores multiselección con saltos de línea rompe el round trip?
Unir las selecciones en una cadena tira los bordes entre valores, y un valor puede contener el separador, así que ningún import puede redividir la cadena correctamente. ISO 32000-1 §12.7.4.4 permite que la entrada /V de un campo de elección sea una única cadena de texto o un array de cadenas, y un list box con el flag MultiSelect (bit 22 de /Ff) usa la forma de array en cuanto se escoge más de una opción. La misma sección define /I como un array de índices de opción 0-based en orden ascendente, que los visores usan para distinguir dos opciones que casualmente comparten valor de export. En HotPDF el getter escalar GetFormFieldValue solo lee la forma de cadena, así que pasar un array por él degradaba el export a una cadena vacía, y el viejo import XFDF unía los elementos <value> repetidos con LF. Imagine una opción exportada como Deep, salto de línea, Blue: tras unir, Deep\nBlue\nRed podrían ser dos selecciones o tres, y el archivo no da forma de saber cuál. El fix fue dejar de usar un escalar en mitad del round trip del todo
¿Qué contienen los archivos FDF y XFDF exportados?
HotPDF escribe un valor multiselección como un array tipado en FDF y como un elemento <value> por selección en XFDF, así que los bordes se quedan visibles en disco. En FDF cada elemento conserva la grafía que tenía en el PDF de origen: las cadenas hexadecimales salen como hex, y las cadenas literales se escapan con un único helper que convierte CR y LF en \r y \n. En XFDF la raíz lleva xml:space="preserve" como exige ISO 19444-1, lo que significa que cualquier espacio en blanco dentro de un elemento de texto cuenta como dato. HotPDF escribe por tanto la etiqueta de apertura, el texto escapado y la etiqueta de cierre de cada <value> de una pieza, 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 cambiar los bytes originales
<!-- 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 límite del export conviene conocer antes de escribir el código que llama. Primero, ExportLoadedFormToFDF construye el cuerpo FDF completo en memoria antes de crear el archivo de destino (corregido en 2.755.1), así que un valor que no se puede exportar, como un array que lleva algo que no sean cadenas, lanza excepción sin truncar un archivo existente. Segundo, una selección vacía en un list box que además ofrece un valor de export de cadena vacía es ambigua en XFDF, porque <value/> podría significar que no hay nada seleccionado o que la opción vacía está seleccionada. ExportLoadedFormToXFDF lanza excepción en ese caso en lugar de adivinar, y lanza antes de abrir el archivo de destino. FDF no tiene tal ambigüedad, porque /V [] y /V [()] son distintos. Ambos exporters de FDF también se saltan los terminales de solo widget que no tienen nombre /T, igual que el exporter XFDF, porque ningún import podría volver a emparejar esas entradas 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 export vacía: XFDF no distingue
// entre ellas, y el archivo .xfdf existente queda intacto
ShowMessage('XFDF export refused: ' + E.Message);
end;
end;
finally
Pdf.Free;
end;
end;
¿Cómo valida HotPDF un valor multiselección en la import?
HotPDF acepta un array importado solo cuando el destino es un campo de elección con el flag MultiSelect puesto y cada valor del array coincide con un valor de export del array /Opt del campo. Cada hueco de opción puede usarse una vez, así que una lista con dos opciones que comparten el valor de export b acepta [<62> <62>] como dos selecciones distintas y rechaza un tercer b. El /I reconstruido sigue el orden de /Opt y no el de los valores entrantes, como exige §12.7.4.4 de índices ascendentes. HotPDF construye el nuevo /V y /I como objetos sueltos y los asigna solo después de que cada valor pase la validación, así que un valor rechazado nunca deja medio array ni índices obsoletos. La copia se escribe al campo que se importa y no a un array de un ancestro compartido, las grafías hex que llegan de FDF se quedan hex durante el guardado, y los campos cuyos cálculos dependen del list box se marcan para recálculo. Si solo necesita fijar un valor, fijar el valor de un campo de formulario en un PDF cargado va por el camino escalar, que por diseño no maneja selecciones múltiples
Algunas otras herramientas escriben valores de export ASCII planos como cadenas hex sin byte order mark, por ejemplo <416272>, y luego exportan XFDF escribiendo esos dígitos hex como texto. Una comparación literal estricta a la vuelta falla, y la import aborta. La versión 2.755.1 añade un reintento: cuando un valor no coincide con ninguna opción, HPDFHexSpellingText decodifica el texto como payload hex y compara el resultado de nuevo. El reintento solo aplica a entrada que de otro modo habría lanzado, así que nunca cambia un valor que ya coincidía. La misma release hizo además que los caminos escalar y de array usen el mismo decodificador Unicode, que entiende PDFDocEncoding, UTF-16 con cualquiera de los dos byte order marks y UTF-8. Antes, un valor lógico podía coincidir por un camino y fallar por el otro en documentos que mezclaban encodings
¿Por qué un archivo FDF válido puede seguir perdiendo campos al parsear?
Un scanner FDF que no rastrea las cadenas hexadecimales puede cortar un diccionario de campo por la mitad cuando un valor hex termina justo al lado del terminador del diccionario. En << /T (region) /V <416273>>> el primer > cierra la cadena hex, pero un scanner ingenuo lo lee junto con el siguiente > como el final del diccionario y suelta el campo en silencio. El import FDF a nivel de archivo ya llevaba la cuenta de si estaba dentro de una cadena hex, y en 2.755.1 los scanners de array y diccionario tras ImportLoadedInterchangeFromFDF hacen lo mismo. Un segundo asunto toca las referencias indirectas. Un archivo FDF es un documento pequeño de sintaxis PDF con numeración de objetos propia (ISO 32000-1 §12.7.7), así que un valor como /V [11 0 R] se refiere al objeto 11 del archivo FDF, no al objeto 11 del PDF que usted está rellenando. El parser FDF simplificado de HotPDF no resuelve referencias dentro del archivo, así que rechaza semejante array en lugar de leer el objeto 11 que casualmente haya en el documento de destino
Las import de archivo, stream y XFDF reportan errores de forma distinta
Las tres rutas de import validan igual pero reportan los fallos distinto, y conviene escoger una a sabiendas. ImportLoadedFormFromFDF se salta cualquier campo que falle la validación y devuelve el número de campos que sí aplicó, así que un conteo menor del esperado es la única señal de problema. ImportLoadedInterchangeFromFDF y ImportLoadedFormFromXFDF lanzan excepción en el primer campo rechazado. Cada campo se compromete por su cuenta, así que los campos procesados antes de la excepción conservan sus valores nuevos. No trate ninguna de estas como transacción sobre el archivo de intercambio entero: si necesita todo-o-nada, descarte el documento cargado cuando salte una excepción en vez de guardarlo
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 un destino no multiselección lanza excepción
if Pdf.ImportLoadedInterchangeFromFDF(Source, True, False, Status, Info) then
Pdf.SaveLoadedDocument('order-form-filled.pdf');
except
on E: Exception do
ShowMessage('Import rejected, nothing saved: ' + E.Message);
end;
finally
Source.Free;
Pdf.Free;
end;
end;
Extender los callbacks XFDF sin romper a quienes ya llaman
El soporte de arrays en la unit XFDF de más bajo nivel vive en un record aparte, THPDFXFDFArrayAccess, y en overloads nuevos de HPDFXFDFExportFields y HPDFXFDFImportFields, no en campos extra añadidos al final del record THPDFXFDFAccess existente. La razón es la compatibilidad binaria. El código que rellena THPDFXFDFAccess como variable local suele fijar solo los slots que conoce y nunca limpia el resto, así que un nuevo puntero a función añadido a ese record contendría basura de stack, y la librería lo tomaría por un callback de verdad. Con un record aparte, los callers viejos conservan el layout viejo y los overloads viejos, y esos overloads pasan internamente un record de arrays todo-nil. El overload de import escalar original sigue uniendo valores repetidos con LF por compatibilidad, y solo el overload consciente de arrays los mantiene separados. Cuando enlace su propio almacén de datos, parta de Default(THPDFXFDFArrayAccess). Devuelva True desde GetFormFieldValueArray para cualquier campo con valor de lista, incluido uno sin nada seleccionado, y False para caer al callback escalar
uses HPDFXFDF;
// Puntero de función llano, no "of object": Context lleva su propio almacén
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 bindings escalares existentes
ArrayAccess := Default(THPDFXFDFArrayAccess); // cada slot sin usar es nil
ArrayAccess.GetFormFieldValueArray := StoreGetSelections;
HPDFXFDFExportFields(Access, ArrayAccess, Bytes);
end;
El intercambio multiselección funciona sobre list boxes que ya existen y tienen el bit MultiSelect puesto en /Ff. Para cómo se crean en primer lugar los campos de elección y sus bits de flag, vea añadir campos ListBox y demás AcroForm a un PDF cargado. Para el markup de comentarios que va por el árbol <annots> de XFDF, vea import y export de anotaciones XFDF en HotPDF. La referencia completa de la API y la descarga de prueba están en la página del componente PDF HotPDF para Delphi