HotPDF hace ida y vuelta de los valores multiselección de un list box por FDF y XFDF manteniendo el valor del campo como array de punta a punta. Desde la versión 2.755.0, ExportLoadedFormToFDF, ExportLoadedInterchangeToFDF y ExportLoadedFormToXFDF escriben cada opción seleccionada como su propio string FDF o elemento <value> de XFDF, y los métodos de importación correspondientes chequean cada valor contra las opciones del campo y reconstruyen los índices de selección /I antes de cambiar nada. Nada queda pegado en un solo string en el camino
La falla 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 elija 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 exportación contenía un salto de línea, y el viejo camino había aplanado las selecciones en un solo string separado por líneas. Recuperar selecciones múltiples de ese string nunca fue confiable, y con un valor de exportación que ya contiene un salto de línea no puede funcionar en absoluto
¿Por qué unir valores multiselección con saltos de línea rompe la ida y vuelta?
Unir las selecciones en un solo string tira por la borda los límites entre valores, y un valor puede contener el separador, así que ningún importador puede redividir el string correctamente. ISO 32000-1 §12.7.4.4 permite que la entrada /V de un campo de elección sea un único string de texto o un array de strings de texto, y 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 como un array de índices de opción base cero en orden ascendente, que los visores usan para distinguir dos opciones que casualmente comparten un valor de exportación. En HotPDF el getter escalar GetFormFieldValue solo lee la forma de string, así que pasar un array por él degradaba la exportación a un string vacío, y la vieja importación 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ía ser dos selecciones o tres, y el archivo no da manera de saber cuál. El arreglo fue dejar de usar un escalar en plena ida y vuelta
¿Qué contienen los archivos FDF y XFDF exportados?
HotPDF escribe un valor multiselección como array tipado en FDF y como un elemento <value> por selección en XFDF, así que los límites quedan visibles en disco. En FDF cada ítem conserva la grafía que tenía en el PDF de origen: los strings hexadecimales salen como hex, y los strings literales los escapa 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 por eso escribe de una pieza el tag de apertura, el texto escapado y el tag de cierre de cada <value>, 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 borde de la exportación conviene conocer antes de escribir el código que llama. Primero, ExportLoadedFormToFDF arma el body FDF completo en memoria antes de crear el archivo de destino (arreglado en 2.755.1), así que un valor que no se puede exportar, como un array que contiene algo que no sean strings, 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 exportación de string vacío 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 en ese caso en lugar de adivinar, y lanza antes de abrir el archivo de destino. FDF no tiene tal ambigüedad, ya que /V [] y /V [()] son distintos. Ambos exportadores FDF también se saltan los terminales de solo widget que no tienen nombre /T, igual que el exportador XFDF, porque ningún importador podría jamás casar 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 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 valida HotPDF un valor multiselección al importar?
HotPDF acepta un array importado solo cuando el objetivo es un campo de elección con el flag MultiSelect activo y cada valor del array coincide con un valor de exportación del array /Opt del campo. Cada casilla de opción puede usarse una sola vez, así que una lista con dos opciones que comparten el valor de exportación b acepta [<62> <62>] como dos selecciones distintas y rechaza un tercer b. El /I reconstruido sigue el orden de /Opt y no el orden de los valores entrantes, ya que §12.7.4.4 exige índices ascendentes. HotPDF arma el nuevo /V y /I como objetos separados y los asigna solo después de que cada valor haya pasado la validación, así que un valor rechazado nunca deja medio array ni índices viejos detrás. La copia se escribe en el campo que se importa y no en un array ancestro compartido, las grafías hex que llegan de FDF siguen siendo hex hasta 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 exportación ASCII planos como hex strings sin marca de orden de bytes, por ejemplo <416272>, y luego exportan XFDF escribiendo esos dígitos hex como texto. Una comparación literal estricta en el camino de vuelta falla, y la importación aborta. La versión 2.755.1 agrega un reintento: cuando un valor no coincide con ninguna opción, HPDFHexSpellingText decodifica el texto como payload hex y compara el resultado otra vez. El reintento solo aplica a entradas que de lo contrario habrían lanzado excepción, así que jamás cambia un valor que ya coincidía. La misma versión hizo además que el camino escalar y el de array usen el mismo decodificador Unicode, que entiende PDFDocEncoding, UTF-16 con cualquiera de las dos marcas de orden de bytes y UTF-8. Antes de eso, un mismo valor lógico podía coincidir por un camino y fallar por el otro en documentos que mezclaban codificaciones
¿Por qué un archivo FDF válido todavía puede perder campos al parsearse?
Un scanner FDF que no sigue los strings hexadecimales puede partir por la mitad un diccionario de campo cuando un valor hex termina pegado al terminador del diccionario. En << /T (region) /V <416273>>> el primer > cierra el string hex, pero un scanner ingenuo lo lee junto con el siguiente > como el fin del diccionario y tira el campo sin avisar. El importador FDF a nivel de archivo ya llevaba cuenta de si estaba dentro de un string hex, y en 2.755.1 los scanners de array y de diccionario detrás de ImportLoadedInterchangeFromFDF hacen lo mismo. Un segundo tema son las referencias indirectas. 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 un valor como /V [11 0 R] se refiere al objeto 11 del archivo FDF, no al objeto 11 del PDF que usted está llenando. 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 importaciones de archivo, stream y XFDF reportan errores distinto
Las tres rutas de importación validan igual pero reportan las fallas distinto, y vale la pena elegir una a conciencia. ImportLoadedFormFromFDF se salta cualquier campo que falle la validación y devuelve la cantidad de campos que sí aplicó, así que un conteo menor al esperado es la única señal de problema. ImportLoadedInterchangeFromFDF y ImportLoadedFormFromXFDF lanzan excepción en el primer campo rechazado. Cada campo se confirma por su cuenta, así que los campos procesados antes de la excepción conservan sus valores nuevos. No trate ninguna de estas como una transacción sobre el archivo de intercambio completo: si necesita comportamiento todo-o-nada, descarte el documento cargado cuando ocurra 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 objetivo no multiselección lanza
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 los callers existentes
El soporte de arrays en la unidad XFDF de más bajo nivel vive en un record separado, THPDFXFDFArrayAccess, y en nuevos overloads de HPDFXFDFExportFields y HPDFXFDFImportFields, no en campos extra agregados al final del record existente THPDFXFDFAccess. La razón es la compatibilidad binaria. Código que llena THPDFXFDFAccess como variable local suele fijar solo los slots que conoce y jamás limpia el resto, así que un puntero a función nuevo agregado a ese record contendría basura del stack, y la biblioteca lo tomaría por un callback real. Con un record separado, los callers viejos conservan el layout viejo y los overloads viejos, y esos overloads pasan internamente un record de array todo nil. El overload original de importación escalar sigue uniendo valores repetidos con LF por compatibilidad, y solo el overload que entiende de arrays los mantiene separados. Cuando enchufe 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 a función simple, 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 activo en /Ff. Para cómo se crean en un principio los campos de elección y sus bits de flag, vea agregar ListBox y otros campos AcroForm a un PDF cargado. Para el markup de comentarios que pasa por el árbol <annots> de XFDF, vea importación y exportación de anotaciones XFDF en HotPDF. La referencia completa de la API y la descarga de prueba están en la página del HotPDF Delphi PDF component