Technical Article

Conversione di collegamenti ipertestuali Rich-Text XFA in link PDF in Delphi

L'architettura XFA (XML Forms Architecture) è deprecata. Lo standard ISO 32000-1 la riporta nel §12.7 segnalando che è stata rimossa a partire da PDF 2.0, e i moderni visualizzatori stanno dismettendo i rispettivi motori XFA uno dopo l'altro. Nulla di tutto questo ha però svuotato gli archivi. Moduli governativi, richieste di assicurazione ed estratti conto bancari sono stati creati in formato XFA per quasi vent'anni, e questi file continuano ad arrivare nelle caselle di posta e nei flussi documentali ancora oggi. Quando il visualizzatore precedentemente utilizzato smette di supportarli, il modulo si trasforma in una pagina vuota contenente un messaggio che invita ad aprire il file con un altro lettore. La soluzione definitiva consiste nel consolidare (flatten) l'XFA in contenuto PDF statico riproducibile da qualsiasi lettore.

La parte difficile del consolidamento non risiede nei campi. Le caselle di testo e di controllo si mappano sui widget AcroForm in modo abbastanza lineare. La difficoltà sta nel testo formattato (rich text) che XFA memorizza all'interno di un elemento draw, in un blocco <exData contentType="text/html">. Tale blocco costituisce un sottoinsieme HTML con stili inline e, spesso, collegamenti (anchors). Riprodurlo sulla pagina significa ricreare sia il testo formattato sia i collegamenti ipertestuali attivi, e questi ultimi rappresentano l'elemento su cui la maggior parte delle implementazioni si arrende silenziosamente.

Come si presenta il testo formattato XFA

Un corpo exData è una porzione di XHTML. Un paragrafo corrisponde a un tag <p>; una sequenza di caratteri formattata è un tag <span> con il proprio stile CSS inline per spessore, stile, colore e dimensione; e un collegamento ipertestuale è un tag <a href="..."> che racchiude il testo visibile. Una singola riga può contenere più span consecutivi, ciascuno con formattazioni differenti, e uno di essi può fungere da ancora. Lo stile non è un elemento decorativo trascurabile. Una clausola evidenziata in rosso grassetto in quanto avviso legale deve rimanere in rosso grassetto anche dopo il consolidamento, per evitare che il documento risultante non sia fedele all'originale.

Pertanto, il motore di consolidamento non può trattare il blocco come un'unica stringa. Deve scorrere la struttura inline, determinare lo stile effettivo di ciascuna sequenza sovrapponendo i CSS inline dello span al font di base dell'elemento draw, e disporre le sequenze una dopo l'altra lungo la riga. HotPDF modella ciascuno di questi frammenti disposti come record interno TXFARichRun. Il record contiene il testo della sequenza, lo stile risolto, il rettangolo misurato e, per le ancore, l'indirizzo Href di destinazione.

Disposizione delle sequenze da sinistra a destra

Il posizionamento rappresenta il momento in cui il testo formattato smette di essere un problema di analisi e diventa un problema di composizione tipografica. Le sequenze condividono la stessa riga, perciò ognuna inizia dove termina la precedente. Non esiste markup che registri tali posizioni; è necessario misurarle. La routine interna LayoutRichText del motore misura ogni sequenza con le metriche del font che verrà poi utilizzato per disegnarla, quindi imposta l'offset orizzontale della sequenza sulla somma progressiva delle larghezze di tutte le sequenze precedenti. La prima sequenza inizia all'origine del box draw, la seconda alla larghezza della prima, la terza alla larghezza combinata delle prime due, e così via lungo la riga.

Questo spiega l'importanza dell'allineamento del font per le misurazioni

Questo spiega l'importanza dell'allineamento del font per le misurazioni. La fase di layout misura gli avanzamenti, mentre una fase di rendering separata disegna i glifi. Se queste due fasi non concordano sul font, i box calcolati dal layout non corrisponderanno alla posizione dei glifi disegnati dal renderer. HotPDF li mantiene allineati mappando lo stile risolto di ciascuna sequenza su una specifica di font, tramite l'helper interno RunStyleToFontSpec, in linea con i valori predefiniti del renderer (Arial a 10 punti). L'avanzamento misurato e il testo disegnato concordano, e il box calcolato per la sequenza copre correttamente i caratteri visibili al lettore.

// 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;

Da una sequenza di tipo ancora a un'annotazione PDF Link

Un collegamento ipertestuale in un PDF finito non fa parte del contenuto della pagina. È un oggetto separato, un'annotazione di tipo Link, descritta nello standard ISO 32000-1 §12.5.6.5. L'annotazione ha una voce /Rect che definisce il rettangolo cliccabile sulla pagina e un'azione che si attiva quando si fa clic sul rettangolo. Per un collegamento esterno, si tratta di un'azione URI: /S /URI con l'indirizzo di destinazione come stringa /URI. Il testo visibile sottostante è normale contenuto della pagina; l'annotazione rappresenta l'area attiva invisibile sovrapposta a esso.

Il processo di consolidamento segue esattamente questo modello. Quando una sequenza porta un Href, HotPDF disegna prima il testo formattato, quindi crea un'annotazione Link sopra il box della sequenza. Il punto di ingresso pubblico per tale annotazione è il metodo di pagina AddURILink, che crea l'oggetto /Type /Annot /Subtype /Link con un'azione /URI e restituisce il relativo dizionario di annotazione. Il suo rettangolo corrisponde al box misurato per la sequenza, tradotto dalle coordinate locali dell'elemento draw in coordinate di pagina. Si ottiene così un collegamento posizionato esattamente sopra il testo dell'ancora.

// 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;

Perché l'area attiva deve basarsi sulle larghezze misurate

Si potrebbe pensare di individuare il collegamento cercando il testo visibile sulla pagina e disegnando il rettangolo attorno a ciò che viene trovato. Questo non funziona, e il motivo risiede nel modo in cui viene memorizzato il testo consolidato. Le sequenze formattate sono disegnate con sottoinsiemi di font incorporati (subset fonts). Un subset font rinumera i glifi conservati, perciò lo stream dei contenuti della pagina contiene codici CID esadecimali, non i codici dei caratteri originali. I byte sulla pagina non corrispondono alle lettere lette da un essere umano e non sono ricercabili come testo. Una ricerca per la didascalia dell'ancora non troverebbe nulla, poiché tale didascalia non esiste come testo letterale all'interno dello stream.

L'unico punto di riferimento affidabile per il rettangolo è la geometria già prodotta dalla fase di layout. L'offset e la larghezza misurata di ogni sequenza sono stati calcolati durante lo scorrimento della riga, prima di qualsiasi rinumerazione dei glifi, e descrivono la posizione fisica in cui il testo apparirà. HotPDF ricava quindi il rettangolo del collegamento direttamente dal box calcolato per la sequenza anziché da ricerche testuali. Poiché la misurazione ha utilizzato il font di rendering, il box risulta corretto indipendentemente dalla creazione dei sottoinsiemi. La geometria sopravvive alla codifica; il testo no. Questa è la motivazione alla base del posizionamento basato sulla larghezza misurata, ed è il motivo per cui un consolidatore che tenta di ricostruire i link tramite ricerca testuale produce aree attive sfasate o del tutto assenti.

Gestire il consolidamento dal codice

Per un PDF che contiene già un pacchetto XFA, il punto di ingresso è FlattenLoadedXFA. Si carica il documento, si chiama il metodo e si salva il risultato. Il parametro Editable stabilisce la sorte dei campi modulo: passa True per mantenerli come widget AcroForm compilabili, o False per impostarli in sola lettura in modo da ottenere un record fisso non modificabile. I blocchi draw di testo formattato, con le rispettive sequenze stilizzate e annotazioni link, vengono generati in entrambi i casi. La funzione restituisce il conteggio dei widget emessi.

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;

Leggi sempre XFAFlattenWarnings dopo la chiamata. L'elenco viene svuotato all'inizio di ciascun consolidamento e raccoglie una riga per ogni elemento che il motore ha escluso dal rendering: un tipo di campo non supportato, un'immagine draw che non è stato possibile decodificare, un blocco exData privo di span utilizzabili. Nessuna di queste situazioni solleva un'eccezione, pertanto un elenco di avvisi vuoto indica che la mappatura è avvenuta completamente, mentre un elenco non vuoto segnala esattamente quali elementi originali esaminare. Se disponi dell'XFA grezzo in formato byte XDP anziché di un PDF caricato, il metodo correlato ApplyXFAAsAcroForm accetta direttamente tali byte condividenti lo stesso percorso di codice e comportamento per gli avvisi. Il metodo complementare AddXFAPacket agisce in direzione opposta, incorporando un pacchetto XFA in un documento in fase di creazione.

Verificare il risultato in un lettore

Apri il file consolidato in Acrobat, o in qualsiasi visualizzatore corrente, e verifica due aspetti. Primo, che il testo formattato sia renderizzato mantenendo gli stili: le sequenze in grassetto devono essere in grassetto, quelle colorate devono avere il proprio colore e gli span devono essere disposti nell'ordine corretto sulla riga, senza sovrapposizioni o uscite dai limiti del box. Secondo, che i collegamenti ipertestuali siano attivi. Passando il mouse sopra un'ancora, la barra di stato deve mostrare l'indirizzo di destinazione; facendo clic, l'azione URI deve aprirlo. Utilizza l'ispettore delle annotazioni del visualizzatore per confermare che ciascun elemento sia una vera annotazione /Link il cui rettangolo /Rect racchiude il testo dell'ancora, posizionata sopra elementi che ora sono semplici glifi disegnati anziché XFA renderizzato dal modulo. Questa combinazione, ovvero testo statico formattato e annotazioni Link reali sui rettangoli corretti, è ciò che consente al documento consolidato di sopravvivere ai motori XFA di cui non ha più bisogno.

Il consolidamento dei campi veri e propri (caselle di testo, di controllo ed elenchi di opzioni che circondano il testo formattato) è trattato nella nostra guida sul consolidamento di moduli XFA in widget AcroForm. Per la gestione completa della creazione e del posizionamento manuale di annotazioni Link, al di là di quelle generate dal consolidamento, si veda la sezione lavorare con le annotazioni PDF in HotPDF. Entrambi gli scenari si basano sullo stesso modello di annotazioni e moduli fornito con l'HotPDF Component per Delphi e C++Builder.