Artículo técnico

Campos PDF multiselección en round trips FDF y XFDF (Delphi)

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

Vieja ida y vuelta multiselección de HotPDF donde dos opciones de list box elegidas, una con un salto de línea incrustado, son aplanadas por el camino escalar de GetFormFieldValue en el string único Deep, salto de línea, Blue, salto de línea, Red, que los lectores aguas abajo pueden interpretar como dos selecciones o como tres
Unir valores multiselección en un solo string destruye los límites entre valores, y un valor de exportación que ya contiene un salto de línea vuelve ambigua la forma aplanada

¿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

Formas de exportación que HotPDF escribe para un list box multiselección: FDF lleva un array tipado por campo con /V [(Deep salto de línea Blue) (Red)] y un valor de región en hex, mientras que XFDF lleva un elemento value por selección bajo xml:space preserve para que el espacio en blanco cuente como dato
Los límites quedan visibles en disco: FDF guarda cada selección como su propio ítem del array y XFDF escribe cada una en un elemento value aparte, así que ningún importador tiene que adivinar
<!-- 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&#xA;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

Validación de importación de HotPDF para valores multiselección: el objetivo debe ser un campo de elección con MultiSelect activo en /Ff, cada valor entrante debe coincidir con un valor de exportación de /Opt con cada slot usado una vez, /I se reconstruye ascendente en orden de /Opt, y /V e /I sueltos se asignan solo después de que todos los valores pasan
Cada valor entrante se chequea contra las opciones del campo antes de escribir nada, así que un valor rechazado nunca deja medio array ni índices de selección viejos

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