Technical Article

Aplanamiento de hipervínculos de texto enriquecido XFA a enlaces PDF en Delphi

XFA, la Arquitectura de Formularios XML (XML Forms Architecture), está obsoleta. La norma ISO 32000-1 la incluye en la sección §12.7 con la nota de que se elimina de PDF 2.0, y los visores modernos están retirando sus motores XFA uno por uno. Nada de eso ha vaciado los archivos históricos. Los formularios gubernamentales de registro, las solicitudes de seguros y los estados de cuenta bancarios se crearon como XFA durante la mayor parte de dos décadas, y esos archivos siguen llegando a las bandejas de entrada y flujos de trabajo de documentos en la actualidad. Cuando el visor que solía renderizarlos deja de hacerlo, el formulario se convierte en una página en blanco con un marcador de posición que dice \"abra en un lector diferente\". La solución duradera consiste en aplanar el XFA en contenido PDF estático que cualquier lector pueda dibujar.

La sección difícil de ese aplanado no son los campos. Las casillas de texto y las casillas de verificación se mapean en widgets de AcroForm de manera bastante limpia. Lo complicado es el texto enriquecido que XFA almacena dentro de un elemento de dibujo, en un bloque <exData contentType=\"text/html\">. Ese bloque es un subconjunto de HTML con estilos en línea y, a menudo, enlaces (anchors). Llevarlo a la página implica reproducir tanto el texto con estilo como los hipervínculos activos, y los hipervínculos son el punto donde la mayoría de las implementaciones se rinden silenciosamente.

Cómo se ve realmente el texto enriquecido de XFA

Un cuerpo exData es una pequeña sección de XHTML. Un párrafo es un <p>; un fragmento de caracteres con estilo es un <span> con su propio CSS en línea para peso, postura, color y tamaño; y un hipervínculo es un <a href=\"...\"> que envuelve su texto visible. Una sola línea puede contener varios fragmentos en fila, cada uno con estilos diferentes, y uno de ellos puede ser un enlace. El estilo no es un elemento decorativo que pueda omitirse. Una cláusula renderizada en rojo negrita porque constituye una advertencia legal debe permanecer en negrita y roja después del aplanado, o el documento aplanado representará de forma incorrecta el original.

Organizar los segmentos de izquierda a derecha

Los segmentos comparten una línea, por lo que cada uno comienza donde terminó el anterior. No hay marcas que registren esas posiciones; deben medirse. La rutina interna LayoutRichText del motor mide cada segmento con las mismas métricas de fuente que se usarán para pintarlo posteriormente, y luego establece el desplazamiento horizontal del segmento según la suma acumulada de todos los anchos de segmento anteriores. El segmento uno comienza en el origen del cuadro de dibujo, el segmento dos comienza en el ancho del segmento uno, el segmento tres en el ancho combinado de los dos primeros, y así sucesivamente a lo largo de la línea.

Es por esto que la alineación de la fuente de medición es tan importante. El paso de diseño mide los avances; un paso de renderizado separado ejerce los glifos. Si estos dos pasos no coinciden en la fuente, las cajas que calculó el diseño no se ubicarán debajo de los glifos que pinta el renderizador. HotPDF los mantiene en sincronía mapeando el estilo resuelto de cada segmento en una especificación de fuente, a través del ayudante interno RunStyleToFontSpec, que coincide con los valores predeterminados del renderizador de Arial a 10 puntos. El avance medido y el texto dibujado coinciden, y la caja calculada de un segmento cubre genuinamente los caracteres que ve el lector.

// Conceptual shape of one laid-out run. The engine builds an array of these
// internally; you never construct them yourself, but the fields explain how a
// link's hit box is derived from measured geometry rather than from text.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // top-left, relative to the draw-box origin
    W, H   : Double;       // measured run box (width from the layout pass)
    Text   : AnsiString;   // the run's visible characters
    Href   : AnsiString;   // URI target for an <a> run, '' otherwise
  end;

De un segmento de enlace a una anotación de enlace PDF

Un hipervínculo en un PDF terminado no es parte del contenido de la página. Es un objeto separado, una anotación de enlace (Link), descrita en ISO 32000-1 §12.5.6.5. La anotación tiene un /Rect que define el rectángulo interactivo en la página y una acción que se ejecuta cuando se hace clic en él. Para un enlace externo, la acción es una acción URI: /S /URI con la dirección de destino como su cadena /URI. El texto visible debajo es contenido ordinario de la página; la anotación es la zona interactiva invisible colocada sobre él.

La ruta de aplanamiento sigue exactamente este modelo. Cuando un segmento lleva un Href, HotPDF dibuja primero el texto con estilo y luego construye una anotación de enlace sobre la caja del segmento. El punto de entrada público para esa anotación es el método de página AddURILink, que crea el objeto /Type /Annot /Subtype /Link con una acción /URI y devuelve el diccionario de anotación. Su rectángulo es la caja medida del segmento, traducida desde las coordenadas locales del elemento de dibujo a coordenadas de la página. El resultado es un enlace que se ubica exactamente sobre el texto del anclaje y en ningún otro lugar.

// The same public API the flatten path uses for each anchor run. It produces
// an ISO 32000-1 12.5.6.5 Link annotation: /Subtype /Link with a /URI action
// over the given rectangle. The optional description fills /Contents so a
// screen reader can announce the target.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // page-space hit box for the run
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

Por qué la caja interactiva debe provenir de los anchos medidos

Es tentador imaginar la localización del enlace buscando en la página su texto visible y dibujando el rectángulo alrededor de lo que se encuentre. Eso no funciona, y la razón es fundamental para la forma en que se almacena el texto aplanado. Los segmentos con estilo se pintan utilizando subconjuntos de fuentes incrustadas. Un subconjunto de fuente renumera los glifos que conserva, por lo que el flujo de contenido de la página contiene códigos hexadecimales CID, no los códigos de caracteres originales. Los bytes en la página no son las letras que lee un ser humano y no se pueden buscar como texto. Una búsqueda de la etiqueta del anclaje no arrojará nada, porque esa etiqueta no existe como texto literal en ninguna parte del flujo.

La única referencia confiable para el rectángulo es la geometría que el paso de diseño ya generó. El desplazamiento y el ancho medido de cada segmento se calcularon al fluir la línea, antes de que se renumerara cualquier glifo, y describen dónde aparecerá físicamente el texto. Por lo tanto, HotPDF toma el rectángulo del enlace directamente de la caja trazada del segmento, en lugar de realizar una búsqueda de texto. Dado que la medición utilizó la fuente de renderizado, la caja es correcta independientemente de la creación del subconjunto. La geometría sobrevive a la codificación; el texto no. Este es el argumento principal para el posicionamiento basado en el ancho medido, y es la razón por la cual un aplanador que intenta incorporar enlaces mediante búsqueda de texto produce zonas interactivas que se desplazan o desaparecen.

Controlar el aplanamiento desde su código

Para un PDF que ya contiene un paquete XFA, el punto de entrada es FlattenLoadedXFA. Cargue el documento, llame al método y guarde el resultado. El parámetro Editable decide qué sucede con los campos de formulario: pase True para mantenerlos como widgets de AcroForm rellenables, o False para marcar cada widget como de solo lectura de modo que el resultado sea un registro congelado. Los bloques de dibujo de texto enriquecido, con sus segmentos de estilo y anotaciones de enlace, se producen en ambos casos. La función devuelve la cantidad de widgets que emitió.

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True keeps fields fillable; False freezes them read-only.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Anything the engine could not map is reported, not raised.
    for i := 0 to Pdf.XFAFlattenWarnings.Count - 1 do
      Writeln('XFA warning: ', Pdf.XFAFlattenWarnings[i]);

    Pdf.SaveLoadedDocument('appeal_form_flat.pdf');
    Writeln('Widgets emitted: ', Emitted);
  finally
    Pdf.Free;
  end;
end;

Siempre lea XFAFlattenWarnings después de la llamada. La lista se limpia al inicio de cada aplanado y acumula una línea por cada elemento que el motor rechazó renderizar: un tipo de campo no admitido, una imagen de dibujo que no se pudo decodificar o un bloque exData sin fragmentos utilizables. Ninguno de estos eventos genera una excepción, por lo que una lista de advertencias vacía es su prueba de que todo se mapeó correctamente, y una lista no vacía le indica exactamente qué originales inspeccionar. Cuando maneje el XFA en bruto como bytes XDP en lugar de un PDF cargado, el método hermano ApplyXFAAsAcroForm toma esos bytes directamente y comparte la misma ruta de código y el mismo comportamiento de advertencias. El método complementario AddXFAPacket funciona a la inversa, incrustando un paquete XFA en un documento que esté construyendo.

Confirmar el resultado en un lector

Abra el archivo aplanado en Acrobat, o en cualquier visor actual, y verifique dos cosas. Primero, que el texto enriquecido se haya renderizado conservando su estilo: los segmentos en negrita están en negrita, los segmentos en color mantienen su color y los tramos se ubican en el orden correcto en la línea en lugar de superponerse o salirse del cuadro. Segundo, que los hipervínculos estén activos. Al pasar el cursor sobre un anclaje, la barra de estado debe mostrar la dirección de destino; haga clic y la acción URI debería abrirlo. Utilice el inspector de anotaciones del visor para confirmar que cada uno es una anotación de tipo /Link real cuyo /Rect rodea el texto del anclaje, ubicado sobre contenido que ahora son simples glifos pintados en lugar de un XFA renderizado por el formulario. Esa combinación, texto estático con estilo más anotaciones de enlace reales en los rectángulos correctos, es lo que permite que el documento aplanado sobreviva a los motores XFA que ya no necesita.

El aplanado de los campos mismos, los cuadros de texto, las casillas de verificación y las listas de opciones que rodean este texto enriquecido, se cubre en nuestro recorrido sobre el aplanado de formularios XFA en widgets de AcroForm. Para la perspectiva más amplia sobre la creación y colocación manual de anotaciones de enlace, más allá de las que genera la ruta de aplanamiento, consulte cómo trabajar con anotaciones PDF en HotPDF. Ambos procesos se basan en el mismo modelo de anotaciones y formularios que se distribuye con el HotPDF Component para Delphi y C++Builder.