Technical Article

Aplatizarea hyperlinkurilor text formatat XFA în linkuri PDF în Delphi

XFA, XML Forms Architecture, este învechită (deprecated). ISO 32000-1 o menționează în §12.7 cu nota că este eliminată din PDF 2.0, iar vizualizatoarele moderne renunță la motoarele lor XFA unul câte unul. Nimic din toate acestea nu a golit arhivele. Formularele guvernamentale de înscriere, cererile de asigurare și extrasele bancare au fost create ca XFA timp de aproape două decenii, iar aceste fișiere sosesc și astăzi în căsuțele poștale și în fluxurile de lucru cu documente. Când vizualizatorul care obișnuia să le redea nu mai face acest lucru, formularul se transformă într-o pagină goală cu un mesaj de substituție care vă solicită să îl deschideți într-un alt cititor. Soluția durabilă este aplatizarea XFA-ului în conținut PDF static pe care orice cititor îl poate picta.

Partea dificilă a acestei aplatizări nu o reprezintă câmpurile. Casetele de text și casetele de selectare se mapează destul de curat pe widgeturile AcroForm. Partea dificilă este textul formatat (rich text) pe care XFA îl stochează în interiorul unui element de desenare, într-un bloc <exData contentType="text/html">. Acel bloc este un subset HTML cu stilizare inline și, adesea, ancore (hyperlinkuri). Redarea sa pe pagină înseamnă reproducerea atât a textului stilizat, cât și a hyperlinkurilor active, iar hyperlinkurile sunt elementele la care majoritatea implementărilor renunță în mod discret.

Cum arată de fapt textul formatat XFA

Un corp exData este o mică porțiune de XHTML. Un paragraf este un <p>; un span de caractere stilizat este un <span> cu propriul CSS inline pentru grosime, formă, culoare și dimensiune; iar un hyperlink este un <a href="..."> care îmbracă textul său vizibil. O singură linie poate conține mai multe span-uri la rând, fiecare cu stilizare diferită, iar unul dintre ele poate fi o ancoră. Stilizarea nu este o decorațiune care poate fi eliminată. O clauză redată cu roșu îngroșat (bold) pentru că este o avertizare legală trebuie să rămână îngroșată și roșie după aplatizare, altfel documentul aplatizat denaturează originalul.

Prin urmare, motorul de aplatizare nu poate trata blocul ca pe un singur șir. Trebuie să parcurgă structura inline, să rezolve stilul efectiv al fiecărei rulări (run) prin suprapunerea CSS-ului inline al span-ului peste fontul de bază al elementului de desenare și să așeze rulările una după alta de-a lungul liniei. HotPDF modelează fiecare dintre aceste fragmente așezate ca pe o înregistrare internă TXFARichRun. Înregistrarea poartă textul rulării, stilul său rezolvat, caseta sa măsurată și, pentru o ancoră, Href-ul către care indică.

Așezarea rulărilor de la stânga la dreapta

Poziționarea este etapa în care textul formatat încetează să mai fie o problemă de parsare și devine o problemă de culegere (typesetting). Rulările împart aceeași linie, astfel încât fiecare rulare începe unde s-a terminat cea anterioară. Nu există niciun marcaj care să înregistreze acele poziții; ele trebuie măsurate. Rutina internă a motorului LayoutRichText măsoară fiecare rulare cu aceleași metrici de font care o vor picta mai târziu, apoi setează decalajul (offset) orizontal al rulării la suma cumulată a tuturor lățimilor rulărilor anterioare. Prima rulare începe la originea casetei de desenare, a doua începe la lățimea primei rulări, a treia la lățimea combinată a primelor două și așa mai departe de-a lungul liniei.

Acesta este motivul pentru care alinierea fontului de măsurare contează atât de mult. Pasul de așezare măsoară avansurile (advances); un pas de redare separat desenează glifele. Dacă aceste două etape nu se înțeleg asupra fontului, casetele calculate de așezare nu se vor potrivi sub glifele pe care le pictează redatorul. HotPDF le menține în pas prin maparea stilului rezolvat al fiecărei rulări pe o specificație de font, prin intermediul ajutorului intern RunStyleToFontSpec, care se potrivește cu valorile implicite ale redatorului pentru Arial la 10 puncte. Avansul măsurat și textul desenat sunt atunci în concordanță, iar caseta calculată a unei rulări acoperă cu adevărat caracterele pe care le vede cititorul.

// 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 la o rulare ancoră la o adnotare de tip PDF Link

Un hyperlink într-un PDF finalizat nu face parte din conținutul paginii. Este un obiect separat, o adnotare de tip Link, descrisă în ISO 32000-1 §12.5.6.5. Adnotarea are un /Rect care definește dreptunghiul pe care se poate face clic pe pagină și o acțiune care se declanșează atunci când dreptunghiul este accesat. Pentru un link extern, acțiunea este o acțiune de tip URI: /S /URI cu adresa țintă ca șir /URI. Textul vizibil de dedesubt este conținut obișnuit al paginii; adnotarea este zona activă invizibilă așezată peste el.

Calea de aplatizare urmează exact acest model. Când o rulare poartă un Href, HotPDF desenează mai întâi textul stilizat, apoi construiește o adnotare Link peste caseta rulării. Punctul de intrare public pentru acea adnotare este metoda de pagină AddURILink, care creează obiectul /Type /Annot /Subtype /Link cu o acțiune /URI și returnează dicționarul adnotării. Dreptunghiul său este caseta măsurată a rulării, translatată din coordonatele locale ale elementului de desenare în coordonatele paginii. Rezultatul este un link care se plasează exact pe textul anrecei și nicăieri altundeva.

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

De ce caseta de selecție (hit box) trebuie să provină din lățimi măsurate

Este tentant să vă imaginați localizarea linkului căutând textul său vizibil pe pagină și desenând dreptunghiul în jurul a ceea ce se găsește. Acest lucru nu funcționează, iar motivul este fundamental pentru modul în care este stocat textul aplatizat. Rulările stilizate sunt pictate cu fonturi subset încorporate. Un font subset renumerotează glifele pe care le păstrează, astfel încât fluxul de conținut al paginii conține coduri hexadecimale CID, nu codurile originale ale caracterelor. Octeții de pe pagină nu sunt literele pe care le citește un om și nu pot fi căutați ca text. O căutare a textului ancorei nu găsește nimic, deoarece acel text nu există ca text literal nicăieri în flux.

Singura ancoră sigură pentru dreptunghi este geometria pe care pasul de așezare a produs-o deja. Decalajul (offset) și lățimea măsurată a fiecărei rulări au fost calculate în timpul curgerii liniei, înainte ca orice glifă să fie renumerotată, și descriu unde va apărea fizic textul. Prin urmare, HotPDF preia dreptunghiul linkului direct din caseta plasată a rulării, mai degrabă decât dintr-o căutare de text. Deoarece măsurătoarea a folosit fontul de redare, caseta este corectă indiferent de subsetting. Geometria supraviețuiește codificării; textul nu. Acesta este întregul argument pentru poziționarea pe bază de lățime măsurată și este motivul pentru care un program de aplatizare care încearcă să adauge linkuri prin căutare de text produce zone active care se deplasează sau dispar.

Controlul aplatizării din codul dumneavoastră

Pentru un PDF care conține deja un pachet XFA, punctul de intrare este FlattenLoadedXFA. Încărcați documentul, apelați metoda și salvați rezultatul. Parametrul Editable decide ce se întâmplă cu câmpurile de formular: transmiteți True pentru a le păstra ca widgeturi fillable AcroForm sau False pentru a marca fiecare widget ca read-only, astfel încât rezultatul să fie o înregistrare înghețată. Blocurile de desenare text formatat, cu rulările lor stilizate și adnotările de link, sunt produse în ambele cazuri. Funcția returnează numărul de widgeturi pe care le-a emis.

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;

Citiți întotdeauna XFAFlattenWarnings după apel. Lista este golită la începutul fiecărei aplatizări și acumulează o linie pentru fiecare element pe care motorul a refuzat să îl redea: un tip de câmp neacceptat, o imagine desenată care nu s-a decodificat, un bloc exData fără span-uri utilizabile. Niciuna dintre acestea nu generează o excepție, așa că o lista de avertismente goală este dovada dumneavoastră că totul a fost mapat, iar una negoală vă spune exact care originale trebuie inspectate. Când dețineți XFA-ul brut ca octeți XDP în loc de un PDF încărcat, metoda soră ApplyXFAAsAcroForm preia acești octeți direct și împarte aceeași cale de cod și același comportament al avertismentelor. Metoda complementară AddXFAPacket merge în sens invers, încorporând un pachet XFA într-un document pe care îl construiți.

Confirmarea rezultatului într-un cititor

Deschideți fișierul aplatizat în Acrobat, sau în orice vizualizator curent, și verificați două lucruri. În primul rând, textul formatat s-a redat cu stilizarea intactă: rulările îngroșate (bold) sunt îngroșate, cele colorate își poartă culoarea, iar span-urile se află în ordinea corectă pe linie, fără a se suprapune sau a ieși din casetă. În al doilea rând, hyperlinkurile sunt active. Treceți cursorul peste o ancoră, iar bara de stare ar trebui să arate adresa țintă; faceți clic pe ea și acțiunea URI ar trebui să o deschidă. Utilizați inspectorul de adnotări al vizualizatorului pentru a confirma că fiecare este o adnotare autentică /Link al cărei /Rect îmbrățișează textul ancorei, fiind așezat peste conținut care este acum doar glife desenate simple, în loc de XFA redat ca formular. Această combinație, text static stilizat plus adnotări Link reale pe dreptunghiurile corecte, este ceea ce face ca documentul aplatizat să supraviețuiască motoarelor XFA de care nu mai are nevoie.

Aplatizarea câmpurilor în sine, a casetelor de text, a casetelor de selectare și a listelor de opțiuni care înconjoară acest text formatat, este acoperită în ghidul nostru despre aplatizarea formularelor XFA în widgeturi AcroForm. Pentru povestea mai largă a construirii și plasării manuale a adnotărilor de tip Link, dincolo de cele generate de calea de aplatizare, consultați lucrul cu adnotările PDF în HotPDF. Ambele se bazează pe același model de adnotări și formulare livrat împreună cu HotPDF Component pentru Delphi și C++Builder.