Artículo técnico

Expresiones de ruta XFA SOM en Delphi: datos de formularios dinámicos

losLab PDF Library lee y escribe nodos de datos repetitivos en formularios XFA dinámicos a través de expresiones de ruta XFA 3.3 SOM: los métodos GetXFAFormFieldValue y SetXFAFormFieldValue aceptan las raíces $data, $record y !data, comodines de ocurrencia [*], selectores de descendientes y comodines de hijos, la propiedad parent, selectores de clase #dataGroup/#dataValue y predicados sencillos de tipo FormCalc, lo que permite que un programa Delphi actualice cada línea de factura en un formulario fiscal del gobierno con una sola llamada. Este artículo es la referencia para el direccionamiento; para mover conjuntos de datos completos entre archivos, consulte el artículo complementario sobre el intercambio de datos de formularios FDF, XFDF y XFA

El problema aparece en el momento en que un formulario deja de ser plano. Una declaración de IVA o de aduanas creada como un formulario XFA dinámico no dispone de doce campos llamados de Total_Price_1 a Total_Price_12; tiene un único subformulario Detail declarado una vez en el DOM de la plantilla e instanciado tantas veces como lo requieran los datos. Los valores residen en un segundo árbol, el DOM de Datos dentro del paquete datasets, donde cada artículo de línea es un elemento <Detail> repetido bajo <Receipt>. Los nombres de campo planos no pueden direccionar "el precio de la tercera línea" o "cada línea superior a 200"; ese es exactamente el cometido que el capítulo Scripting Object Model de la especificación Adobe XFA 3.3 otorga a las expresiones SOM, y es la sintaxis que utilizan las API de valores de campo XFA de losLab PDF Library

¿Cómo direccionar nodos de datos XFA desde Delphi?

Cada ruta de datasets comienza desde una raíz, y losLab PDF Library acepta las nomenclaturas estándar de forma intercambiable: $data es la forma abreviada de XFA 3.3 para xfa.datasets.data, !data es la forma corta enraizada en xfa.datasets, y en paquetes que no utilizan procesamiento de registros, $record se resuelve en el registro de datos externo, el primer elemento bajo el nodo de datos. Las API del lado de la plantilla, como SetXFAFormFieldAccess, aceptan $template y xfa.template de la misma manera. Los segmentos pueden delimitarse con puntos o con barras ($data.Receipt.Tax or $data/Receipt/Tax), lo cual es importante porque GetXFAFormFieldNames enumera los campos como rutas delimitadas por barras que se reintroducen directamente en las API de valores y plantillas. Los índices de ocurrencia se basan en cero según la especificación, por lo que Detail[0] es la primera fila. Dos reglas de escape mantienen direccionables los nombres inusuales: en una ruta delimitada por puntos, \. denota un punto literal (Line\.Item), mientras que las rutas delimitadas por barras tratan los puntos como caracteres ordinarios. Los datos de negocio que contienen su propio prefijo de espacio de nombres XML se asocian por su nombre local, por lo que <m:Receipt> sigue respondiendo a Receipt

var
  Lib: TPDFlib;
  Tax: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('vat-return.pdf', '');
    // Direcciones equivalentes para el mismo nodo de datos
    Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
    Tax := Lib.GetXFAFormFieldValue('$record.Tax');
    // Forma de barra con un índice de ocurrencia basado en cero
    Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
    Lib.SaveToFile('vat-return-updated.pdf');
  finally
    Lib.Free;
  end;
end;

¿Cómo rellenar filas repetitivas en un formulario XFA mediante programación?

El comodín de ocurrencia [*] es la herramienta para procesamiento por lotes. Mientras que un índice numérico selecciona un hermano, [*] selecciona todos los hermanos con el mismo nombre, de modo que $data.Receipt.Detail[*].Total_Price direcciona el campo de precio de cada artículo de línea a la vez. En una lectura, GetXFAFormFieldValue devuelve los valores de todos los nodos coincidentes unidos con un separador |; en una escritura, SetXFAFormFieldValue actualiza cada nodo coincidente con el mismo valor y devuelve 1 si tiene éxito. Una ruta que no coincide con nada se lee como una cadena vacía, que es la forma rápida de verificar si existe una rama antes de escribir en ella

var
  Lib: TPDFlib;
  Prices: WideString;
begin
  Lib := TPDFlib.Create;
  try
    Lib.LoadFromFile('invoice.pdf', '');
    // Todas las filas Detail a la vez: '250.00|60.00'
    Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
    // Restablecer una columna en cada fila repetida en una sola llamada
    Lib.SetXFAFormFieldValue('$data.Receipt.Detail[*].Surcharge', '0.00');
    Lib.SaveToFile('invoice-updated.pdf');
  finally
    Lib.Free;
  end;
end;

Selectores de descendientes, comodines de hijos y padres

Tres selectores estructurales cubren los casos en los que conoce el nombre del campo pero no su profundidad exacta. El selector de descendientes .. busca coincidencias a cualquier profundidad por debajo del nodo actual: $data..Total_Price localiza el primer Total_Price en cualquier lugar bajo la raíz de datos, y una escritura a través de una ruta de descendiente actualiza ese primer nodo coincidente. Tenga en cuenta el límite: un índice de ocurrencia después de una coincidencia de descendiente no avanza por las ramas hermanas, por lo que si $data..Total_Price[0] se resuelve dentro de la primera fila Detail, $data..Total_Price[1] se devuelve vacío en lugar de saltar a la fila siguiente; utilice Detail[*] cuando los quiera todos. El comodín de hijo .* coincide con cada hijo directo y permite filtrar los segmentos restantes: $data.Receipt.*.Total_Price llega al Total_Price dentro de cada rama hija de Receipt sin seleccionar además un campo de resumen con el mismo nombre situado directamente en el propio Receipt. Por último, la propiedad parent sube un nivel, que XFA 3.3 separa deliberadamente de ..: $data.Receipt.Detail[1].parent.Tax comienza en el segundo artículo de línea, sube a Receipt y aterriza en su valor hermano Tax

Selectores de clase y predicados: #dataGroup, #dataValue, .[expresión]

La sintaxis #class direcciona los nodos del DOM de Datos por clase de objeto en lugar de por nombre, lo que representa la vía práctica cuando las etiquetas XML contienen caracteres difíciles de escribir como nombres de script. losLab PDF Library asocia #dataGroup a elementos que tienen hijos de tipo elemento y #dataValue a elementos hoja sin hijos de tipo elemento, y ambos se combinan con ocurrencias numéricas o [*]: $data.Receipt.#dataGroup[0].#dataValue[0] selecciona el primer valor dentro del primer grupo bajo Receipt. El selector de predicados .[expresión] filtra hermanos con el mismo nombre por su contenido. Los predicados admitidos son comparaciones simples de un valor secundario (hijo) con un literal, con operadores relacionales como >, < y <=; cuando un predicado coincide con varias filas, las lecturas se devuelven unidas por | y las escrituras actualizan cada coincidencia

// Marcar cada línea cuyo Total_Price supere los 200
Lib.SetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');

// Descripciones de todas las líneas de bajo valor, unidas por '|' cuando coinciden varias
Desc := Lib.GetXFAFormFieldValue(
  '$data.Receipt.Detail.[Total_Price <= 200].Description');

// Direccionamiento basado en clase cuando los nombres de etiqueta dificultan la escritura del script
Total := Lib.GetXFAFormFieldValue(
  '$data.Receipt.#dataGroup[0].#dataValue[0]');

¿Qué características de XFA SOM no son compatibles?

  • Los predicados no ejecutan FormCalc o JavaScript arbitrarios: no hay llamadas a funciones (un predicado contains(...) se devuelve vacío), no hay expresiones booleanas compuestas, únicamente una comparación simple de hijo operador literal
  • $record funciona únicamente en paquetes sin procesamiento de registros; la paginación de registros dataWindow y la rotación de grupos de registros no están implementadas
  • La resolución se realiza directamente sobre el DOM de Datos y el DOM de la plantilla; no se realiza la resolución relativa del DOM de Formulario, la vista combinada que un visor crea en tiempo de ejecución
  • No se aplican las semánticas de carga de atributos del DOM de Datos; la biblioteca lee el paquete datasets como XML sin procesar, por lo que las rutas direccionan elementos, no atributos promovidos a nodos

Estos límites rara vez causan problemas en tareas de llenado por lotes, ya que el encargado de llenado direcciona los datos por su estructura en lugar de por lógica programada. Cuando lo hacen, la salida de emergencia consiste en trabajar a nivel de paquete: leer todo el XML de datasets, transformarlo en Delphi y volver a escribirlo

Escritura del paquete completo con SetXFAFromString

SetXFAFromString representa ese punto de entrada a nivel de paquete: instala una cadena XDP completa (plantilla y datasets juntos), creando el contenedor AcroForm en un documento nuevo cuando no existe ninguno, y acepta paquetes codificados en UTF-16 con marcas de orden de bytes, así como en UTF-8. Un modelo común en producción consiste en mantener el archivo XDP emitido por la agencia como un recurso fijo, cargarlo con SetXFAFromString y luego ejecutar llamadas SetXFAFormFieldValue direccionadas por SOM para los valores volátiles por factura antes de guardar. Dado que XFA es uno de los dos modelos de formulario que puede contener un PDF, el lado clásico de AcroForm tiene su propio sistema de automatización, cubierto en el artículo sobre acciones de formulario interactivas y JavaScript

Las API de valores de campo, enumeración y paquetes XFA mostradas aquí se distribuyen en la losLab PDF Library para Delphi, C# y VB.NET; la página del producto contiene la referencia completa de gestión de formularios