losLab PDF Library lee y escribe nodos de datos repetidos en formularios XFA dinámicos a través de expresiones de ruta XFA 3.3 SOM: 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 simples al estilo FormCalc, de modo que un programa en Delphi puede actualizar cada línea de factura en un formulario fiscal del gobierno con una sola llamada. Este artículo es la referencia de 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 estructurada como un formulario XFA dinámico no tiene doce campos llamados Total_Price_1 a Total_Price_12; tiene un subformulario Detail declarado una sola vez en el DOM de la plantilla y del cual se crean tantas instancias como los datos lo requieran. Los valores residen en un segundo árbol, el Data DOM dentro del paquete datasets, donde cada línea de detalle es un elemento <Detail> repetido bajo <Receipt>. Los nombres de campo planos no pueden direccionar "el precio en la tercera línea" o "cada línea superior a 200"; ese es exactamente el trabajo que el capítulo Scripting Object Model de la especificación Adobe XFA 3.3 otorga a las expresiones SOM, y es la sintaxis con la que se comunican 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 escrituras estándar indistintamente: $data es la forma abreviada de XFA 3.3 para xfa.datasets.data, !data es la forma abreviada con raíz en xfa.datasets, y en paquetes que no utilizan procesamiento de registros, $record se resuelve como el registro de datos externo, el primer elemento bajo el nodo data. Las API del lado de la plantilla, como SetXFAFormFieldAccess, aceptan $template y xfa.template de la misma manera. Los segmentos pueden delimitarse por puntos o por barras diagonales ($data.Receipt.Tax o $data/Receipt/Tax), lo cual es importante porque GetXFAFormFieldNames enumera los campos como rutas delimitadas por barras que se introducen 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 permiten direccionar nombres inusuales: en una ruta delimitada por puntos, \. denota un punto literal (Line\.Item), mientras que las rutas delimitadas por barras diagonales tratan los puntos como caracteres comunes. Los datos empresariales que llevan su propio prefijo de espacio de nombres XML se buscan 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', '');
// Equivalent addresses for the same data node
Tax := Lib.GetXFAFormFieldValue('Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('$data.Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('xfa.datasets.data.Receipt.Tax');
Tax := Lib.GetXFAFormFieldValue('$record.Tax');
// Slash form with a zero-based occurrence index
Lib.SetXFAFormFieldValue('$record/Detail[0]/Total_Price', '251.00');
Lib.SaveToFile('vat-return-updated.pdf');
finally
Lib.Free;
end;
end;
¿Cómo completar filas repetidas 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 solo elemento hermano, [*] selecciona todos los hermanos con el mismo nombre, por lo que $data.Receipt.Detail[*].Total_Price direcciona el campo de precio en cada línea de detalle 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, lo cual representa la forma rápida de verificar si una rama existe antes de escribir en ella
var
Lib: TPDFlib;
Prices: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.LoadFromFile('invoice.pdf', '');
// All Detail rows at once: '250.00|60.00'
Prices := Lib.GetXFAFormFieldValue('$data.Receipt.Detail[*].Total_Price');
// Reset one column across every repeated row in a single call
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 elementos 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 a cualquier profundidad debajo del nodo actual: $data..Total_Price encuentra el primer Total_Price en cualquier lugar bajo la raíz data, y una escritura a través de una ruta de descendientes actualiza ese primer nodo coincidente. Tenga en cuenta el límite: un índice de ocurrencia después de una coincidencia de descendiente no se desplaza a través de 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 necesite todos. El comodín de hijo .* coincide con cada hijo directo y permite filtrar por los segmentos restantes: $data.Receipt.*.Total_Price llega al Total_Price dentro de cada rama hija de Receipt sin seleccionar también un campo de resumen con el mismo nombre ubicado directamente en la propia Receipt. Finalmente, la propiedad parent sube un nivel, algo que XFA 3.3 separa deliberadamente de ..: $data.Receipt.Detail[1].parent.Tax comienza en la segunda línea de detalle, sube hasta Receipt y aterriza en su valor hermano Tax
Selectores de clase y predicados: #dataGroup, #dataValue, .[expresión]
La sintaxis #class direcciona los nodos Data DOM por clase de objeto en lugar de por nombre, lo cual es la vía práctica cuando las etiquetas XML contienen caracteres que son difíciles de escribir como nombres de script. losLab PDF Library asigna #dataGroup a los elementos que tienen elementos hijos y #dataValue a los elementos hoja que carecen de elementos hijos, y ambos se combinan con ocurrencias numéricas o de tipo [*]: $data.Receipt.#dataGroup[0].#dataValue[0] selecciona el primer valor dentro del primer grupo bajo Receipt. El selector de predicados .[expression] filtra hermanos con el mismo nombre según el contenido. Los predicados admitidos son comparaciones simples de un valor hijo frente a un literal, con operadores relacionales como >, < y <=; cuando un predicado coincide con varias filas, las lecturas devuelven los valores unidos por | y las escrituras actualizan cada coincidencia
// Flag every line whose Total_Price exceeds 200
Lib.SetXFAFormFieldValue(
'$data.Receipt.Detail.[Total_Price > 200].Review_Flag', '1');
// Descriptions of all low-value lines, '|'-joined when several match
Desc := Lib.GetXFAFormFieldValue(
'$data.Receipt.Detail.[Total_Price <= 200].Description');
// Class-based addressing when tag names resist script spelling
Total := Lib.GetXFAFormFieldValue(
'$data.Receipt.#dataGroup[0].#dataValue[0]');
¿Qué características de XFA SOM no están admitidas?
El soporte SOM en losLab PDF Library está acotado a las rutas de campos de datasets y plantillas, y los límites son explícitos en lugar de aproximados. Conocerlos de antemano evita la depuración de una ruta que devuelve silenciosamente una cadena vacía
- Los predicados no ejecutan FormCalc o JavaScript arbitrarios: no hay llamadas a funciones (un predicado
contains(...)se devuelve vacío), ni expresiones booleanas compuestas; solo se permite una única comparación simple de tipohijo operador literal $recordfunciona únicamente en paquetes que no realizan procesamiento de registros; no están implementados la paginación de registros de dataWindow ni la rotación de grupos de registros- La resolución se realiza directamente frente a los DOM de datos (Data DOM) y de plantilla (template DOM); no se lleva a cabo la resolución relativa del DOM del formulario (Form DOM), que es la vista fusionada que un visor construye 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 las tareas de llenado por lotes, ya que un rellenador direcciona los datos por estructura en lugar de por lógica programada. Cuando lo hacen, la salida de escape es trabajar a nivel de paquete: leer todo el XML de datasets, transformarlo en Delphi y volverlo a escribir
Escribir el paquete completo con SetXFAFromString
SetXFAFromString es 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 aún no existe ninguno, y acepta paquetes codificados en UTF-16 con marcas de orden de bytes, así como en UTF-8. Un flujo de producción común consiste en mantener el XDP emitido por la agencia como un recurso fijo, cargarlo con SetXFAFromString y luego ejecutar llamadas a SetXFAFormFieldValue direccionadas por SOM para los valores variables de cada factura antes de guardar. Dado que XFA es uno de los dos modelos de formulario que un PDF puede contener, el lado clásico de AcroForm tiene su propia historia de automatización, analizada en el artículo sobre acciones de formularios interactivos y JavaScript
Las API de valor de campo, enumeración y paquete XFA que se muestran aquí se incluyen en losLab PDF Library para Delphi, C# y VB.NET; la página del producto contiene la referencia completa para el manejo de formularios