Technical Article

XFA rich-text hyperlinks samenvoegen naar PDF-links in Delphi

XFA, de XML Forms Architecture, is verouderd. ISO 32000-1 bevat het in §12.7 met de opmerking dat het is verwijderd uit PDF 2.0, en moderne viewers schaffen hun XFA-engines één voor één af. Niets daarvan heeft de archieven leeggemaakt. Overheidsformulieren, verzekeringsaanvragen en bankafschriften zijn gedurende bijna twee decennia als XFA opgesteld, en die bestanden komen vandaag de dag nog steeds binnen in inboxen en documenttrajecten. Wanneer de viewer die ze placht te renderen ermee ophoudt, verandert het formulier in een lege pagina met een melding 'open dit in een andere lezer'. De duurzame oplossing is om de XFA samen te voegen (flatten) tot statische PDF-inhoud die elke lezer kan weergeven.

Het moeilijke deel van dat samenvoegen zijn niet de velden. Tekstvakken en selectievakjes mappen netjes genoeg naar AcroForm-widgets. Het moeilijke deel is de opgemaakte tekst (rich-text) die XFA opslaat in een tekenelement, in een <exData contentType="text/html">-blok. Dat blok is een HTML-subset met inline opmaak en vaak ankers (anchors). Om dit op de pagina te krijgen, moet zowel de opgemaakte tekst als de live hyperlinks worden gereproduceerd, en die hyperlinks zijn waar de meeste implementaties stilletjes opgeven.

Hoe XFA rich-text er daadwerkelijk uitziet

Een exData-body is een klein stukje XHTML. Een alinea is een <p>; een opgemaakte reeks tekens is een <span> met zijn eigen inline CSS voor dikte, stijl, kleur en grootte; en een hyperlink is een <a href="..."> die de zichtbare tekst omsluit. Een enkele regel kan meerdere spans achter elkaar bevatten, elk met een andere opmaak, en een daarvan kan een anker zijn. De opmaak is geen versiering die kan worden weggelaten. Een bepaling die in vet rood is weergegeven omdat het een juridische waarschuwing is, moet na het samenvoegen vet en rood blijven, anders geeft het samengevoegde document een onjuiste weergave van het origineel.

De flatten-engine kan het blok dus niet als één string behandelen. Het moet de inline structuur doorlopen, de effectieve stijl van elke run bepalen door de inline CSS van de span over het basislettertype van het tekenelement te leggen, en de runs achter elkaar over de regel verdelen. HotPDF modelleert elk van deze opgemaakte fragmenten als een intern TXFARichRun-record. Het record bevat de tekst van de run, de opgeloste stijl, het gemeten kader en, voor een anker, de Href waarnaar het verwijst.

De runs van links naar rechts uitlijnen

Positionering is het punt waar opgemaakte tekst stopt met een parseringsprobleem te zijn en een zetwerkprobleem wordt. De runs delen een regel, dus elke run begint waar de vorige eindigde. Er is geen opmaak die die posities registreert; ze moeten worden gemeten. De interne routine LayoutRichText van de engine meet elke run met dezelfde fontstatistieken die de tekst later zullen tekenen, en stelt vervolgens de horizontale verschuiving (offset) van de run in op de lopende som van alle voorgaande runbreedtes. Run één begint bij de oorsprong van het tekenkader, run twee begint bij de breedte van run één, run drie bij de gecombineerde breedte van de eerste twee, enzovoort over de regel.

Dit is waarom de uitlijning van het meetlettertype zo belangrijk is. De layout-stap meet de voorschotten (advances); een afzonderlijke render-stap tekent glyphs. Als die twee stappen het oneens zijn over het lettertype, zullen de kaders die de layout heeft berekend niet onder de glyphs liggen die de renderer tekent. HotPDF houdt ze in de pas door de opgeloste stijl van elke run te mappen naar een lettertypespecificatie, via de interne helper RunStyleToFontSpec, die overeenkomt met de eigen standaardwaarden van de renderer voor Arial op 10 punten. Het gemeten voorschot en de getekende tekst komen dan overeen, en het berekende kader van een run dekt werkelijk de tekens die een lezer ziet.

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

Van een ankerrun naar een PDF-linkannotatie

Een hyperlink in een definitieve PDF maakt geen deel uit van de pagina-inhoud. Het is een afzonderlijk object, een Link-annotatie, beschreven in ISO 32000-1 §12.5.6.5. De annotatie haalt een /Rect die de klikbare rechthoek op de pagina definieert en een actie die wordt geactiveerd wanneer op de rechthoek wordt geklikt. Voor een externe link is de actie een URI-actie: /S /URI met het doeladres als de /URI-string. De zichtbare tekst eronder is gewone pagina-inhoud; de annotatie is de onzichtbare klikbare zone die eroverheen is gelegd.

Het flatten-pad volgt exact dit model. Wanneer een run een Href bevat, HotPDF eerst de opgemaakte tekst en bouwt vervolgens een Link-annotatie over het kader van de run. Het openbare toegangspunt voor die annotatie is de paginamethode AddURILink, die het /Type /Annot /Subtype /Link-object maakt met een /URI-actie en de annotatiedictionary retourneert. De bijbehorende rechthoek is het gemeten kader van de run, vertaald vanuit de lokale coördinaten van het tekenelement naar paginacoördinaten. Het resultaat is een link dat lands precies op de ankertekst en nergens anders.

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

Waarom de klikbare zone moet voortkomen uit gemeten breedtes

Het is verleidelijk om de link te lokaliseren door op de pagina te zoeken naar de zichtbare tekst en de rechthoek te tekenen rond alles wat wordt gevonden. Dat werkt niet, en de reden is fundamenteel voor de manier waarop samengevoegde tekst wordt opgeslagen. De opgemaakte runs worden getekend met ingebedde subsetlettertypen. Een subsetlettertype hernummert de glyphs die het behoudt, dus de pagina-inhoudsstroom bevat hexadecimale CID-codes, niet de oorspronkelijke tekencodes. De bytes op de pagina zijn niet de letters die een mens leest, en ze zijn niet doorzoekbaar als tekst. Een zoekopdracht naar het bijschrift van het anker vindt niets, omdat dat bijschrift nergens in de stroom als letterlijke tekst bestaat.

Het enige betrouwbare anker voor de rechthoek is de geometrie die de layout-stap al heeft geproduceerd. De offset en gemeten breedte van elke run zijn berekend tijdens het vloeien van de regel, voordat er een glyph werd hernummerd, en ze beschrijven waar de tekst fysiek zal verschijnen. HotPDF neemt de linkrechthoek daarom rechtstreeks over uit het neergelegde kader van de run in plaats van uit een tekstzoekopdracht. Omdat de meting het renderlettertype gebruikte, is het kader correct, ongeacht subsetting. Geometrie overleeft de codering; tekst niet. Dat is het hele argument voor positionering op basis van gemeten breedte, en dat is waarom een flattener die probeert achteraf links te plaatsen via tekstzoekopdrachten, klikbare zones produceert die verschuiven of verdwijnen.

Het samenvoegen aansturen vanuit uw code

Voor een PDF die al een XFA-pakket bevat, is het toegangspunt FlattenLoadedXFA. Laad het document, roep de methode aan en sla het resultaat op. De parameter Editable bepaalt wat er met de formuliervelden gebeurt: geef True door om ze als invulbare AcroForm-widgets te behouden, of False om elke widget als alleen-lezen te markeren zodat de uitvoer een vastgelegd document wordt. De opgemaakte tekstblokken, met hun opgemaakte runs en linkannotaties, worden in beide gevallen geproduceerd. De functie retourneert het aantal widgets dat is verzonden.

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;

Lees na de aanroep altijd XFAFlattenWarnings uit. De lijst wordt leeggemaakt aan het begin van elke flatten en verzamelt een regel voor elk element dat de engine heeft geweigerd te renderen: een niet-ondersteund veldtype, een tekenafbeelding die niet decodeert, een exData-blok zonder bruikbare spans. Geen van deze genereert een uitzondering, dus een lege waarschuwingenlijst is het bewijs dat alles is gemapt, en een niet-lege vertelt u precies welke originelen u moet inspecteren. Wanneer u de ruwe XFA als XDP-bytes hebt in plaats van een geladen PDF, de zustermethode ApplyXFAAsAcroForm accepteert die bytes rechtstreeks en deelt hetzelfde codepad en hetzelfde gedrag voor waarschuwingen. De complementaire methode AddXFAPacket werkt andersom en sluit een XFA-pakket in in een document dat u aan het bouwen bent.

Het resultaat controleren in een lezer

Open het samengevoegde bestand in Acrobat, of een andere huidige viewer, en controleer twee dingen. Ten eerste dat de opgemaakte tekst is gerenderd met behoud van de stijl: de vette runs zijn vet, de gekleurde runs hebben hun kleur behouden en de spans staan in de juiste volgorde op de regel in plaats van elkaar te overlappen of buiten het kader te lopen. Ten tweede dat de hyperlinks live zijn. Beweeg de muis over een anker en de statusbalk moet het doeladres tonen; klik erop en de URI-actie moet deze openen. Gebruik de annotatie-inspecteur van de viewer om te bevestigen dat elk ervan een echte /Link-annotatie is waarvan de /Rect de ankertekst omsluit, liggend over inhoud die nu gewone getekende glyphs is in plaats van door het formulier gerenderde XFA. Die combinatie, opgemaakte statische tekst plus echte Link-annotaties op de juiste rechthoeken, is wat ervoor zorgt dat het samengevoegde document de XFA-engines overleeft die het niet langer nodig heeft.

Het samenvoegen van de velden zelf (de tekstvakken, selectievakjes en keuzelijsten die deze rich-text omringen) wordt behandeld in onze handleiding over het samenvoegen van XFA-formulieren naar AcroForm-widgets. Voor het bredere verhaal over het handmatig bouwen en plaatsen van Link-annotaties, naast degene die het samenvoegpad genereert, zie werken met PDF-annotaties in HotPDF. Beide bouwen voort op hetzelfde annotatie- en formulierenmodel dat wordt geleverd met de HotPDF Component voor Delphi en C++Builder.