Technischer Artikel

Abflachen von XFA-Rich-Text-Hyperlinks zu PDF-Links in Delphi

XFA, die XML Forms Architecture, ist veraltet (deprecated). ISO 32000-1 führt sie in §12.7 mit dem Hinweis, dass sie aus PDF 2.0 entfernt wird, und moderne Viewer (Betrachter) geben ihre XFA-Engines nach und nach auf. Nichts davon hat die Archive geleert. Behördliche Aufnahmeformulare, Versicherungsanträge und Kontoauszüge wurden für den Großteil von zwei Jahrzehnten als XFA erstellt, und diese Dateien landen auch heute noch in Posteingängen und Dokumentenpipelines. Wenn der Viewer, der sie früher gerendert hat, dies nicht mehr tut, verwandelt sich das Formular in eine leere Seite mit einem Platzhalter "Bitte in einem anderen Reader öffnen". Die dauerhafte Lösung besteht darin, das XFA in statischen PDF-Inhalt abzuflachen (flattening), den jeder Reader darstellen (paint) kann

Der schwierige Teil dieses Abflachens sind nicht die Felder. Textfelder und Kontrollkästchen (Check Boxes) lassen sich sauber genug auf AcroForm-Widgets abbilden. Der schwierige Teil ist der Rich-Text, den XFA in einem Draw-Element (Zeichenelement) speichert, in einem <exData contentType="text/html">-Block. Dieser Block ist ein HTML-Subset mit Inline-Styling und oft mit Ankern (Anchors). Um ihn auf die Seite zu bekommen, müssen sowohl der formatierte Text als auch die aktiven (live) Hyperlinks reproduziert werden, und bei den Hyperlinks geben die meisten Implementierungen leise auf

Wie XFA-Rich-Text eigentlich aussieht

Ein exData-Körper (Body) ist ein kleiner Teil von XHTML. Ein Absatz ist ein <p>; ein formatierter Bereich von Zeichen (Span) ist ein <span> mit eigenem Inline-CSS für Strichstärke (Weight), Neigung (Posture), Farbe und Größe; und ein Hyperlink ist ein <a href="...">, das seinen sichtbaren Text umschließt. Eine einzelne Zeile kann mehrere Spans in einer Reihe enthalten, jeweils mit unterschiedlichem Styling, und einer davon kann ein Anker sein. Das Styling ist keine Zierde, die man einfach weglassen kann. Eine Klausel, die fett und rot gerendert wird, weil sie eine rechtliche Warnung ist, muss auch nach dem Abflachen fett und rot bleiben, sonst gibt das abgeflachte Dokument das Original falsch wieder

Die Flatten-Engine kann den Block also nicht als einen einzigen String behandeln. Sie muss die Inline-Struktur durchlaufen, das effektive Styling jedes Textabschnitts (Runs) auflösen, indem sie das Inline-CSS des Spans über die Basisschrift (Base Font) des Draw-Elements legt, und die Runs nacheinander in der Zeile anordnen (lay out). HotPDF modelliert jedes dieser angeordneten Fragmente als internen TXFARichRun-Record (Datensatz). Der Record trägt den Text des Runs, sein aufgelöstes Styling, seine gemessene Box (Bounding Box) und bei einem Anker den Href, auf den er verweist

Anordnen (Layouting) der Runs von links nach rechts

Bei der Positionierung hört Rich-Text auf, ein Parsing-Problem zu sein, und wird zu einem Typografie-Problem (Typesetting). Die Runs teilen sich eine Zeile, also beginnt jeder Run dort, wo der vorherige endete. Es gibt kein Markup, das diese Positionen aufzeichnet; sie müssen gemessen werden. Die interne LayoutRichText-Routine der Engine misst jeden Run mit denselben Font-Metriken (Font Metrics), die ihn später auch zeichnen (paint) werden, und setzt dann den horizontalen Offset des Runs auf die fortlaufende Summe (Running Sum) aller vorherigen Run-Breiten. Run eins beginnt am Ursprung der Draw-Box, Run zwei beginnt bei der Breite von Run eins, Run drei bei der kombinierten Breite der ersten beiden, und so weiter über die Zeile

Darum ist die Ausrichtung der Mess-Schriftart (Measurement Font Alignment) so wichtig. Der Layout-Durchlauf (Layout Pass) misst Vorschübe (Advances); ein separater Render-Durchlauf zeichnet Glyphen. Wenn sich diese beiden Durchläufe bezüglich der Schriftart nicht einig sind, liegen die vom Layout berechneten Boxen nicht unter den Glyphen, die der Renderer zeichnet. HotPDF hält sie synchron, indem es den aufgelösten Stil jedes Runs über den internen Helfer RunStyleToFontSpec auf eine Schriftartenspezifikation abbildet, die den Standardeinstellungen des Renderers von Arial bei 10 Punkt entspricht. Der gemessene Vorschub und der gezeichnete Text stimmen dann überein, und die berechnete Box eines Runs deckt tatsächlich die Zeichen ab, die ein Leser sieht

// Konzeptueller Aufbau eines angeordneten (laid-out) Runs. Die Engine baut ein Array dieser
// intern auf; Sie konstruieren sie niemals selbst, aber die Felder erklären, wie eine
// Trefferzone (Hit Box) eines Links aus gemessener Geometrie statt aus Text abgeleitet wird.
type
  TRichRunInfo = record
    Dx, Dy : Double;       // Oben links, relativ zum Ursprung der Draw-Box
    W, H   : Double;       // Gemessene Run-Box (Breite aus dem Layout-Durchlauf)
    Text   : AnsiString;   // Die sichtbaren Zeichen des Runs
    Href   : AnsiString;   // URI-Ziel für einen <a>-Run, andernfalls ''
  end;

Von einem Anker-Run zu einer PDF-Link-Annotation

Ein Hyperlink in einem fertigen PDF ist kein Teil des Seiteninhalts. Es handelt sich um ein separates Objekt, eine Link-Annotation, die in ISO 32000-1 §12.5.6.5 beschrieben ist. Die Annotation hat ein /Rect, das das klickbare Rechteck auf der Seite definiert, sowie eine Aktion, die ausgelöst (fires) wird, wenn auf das Rechteck geklickt wird. Für einen externen Link ist die Aktion eine URI-Aktion: /S /URI mit der Zieladresse als deren /URI-String. Der sichtbare Text darunter ist gewöhnlicher Seiteninhalt; die Annotation ist die unsichtbare heiße Zone (Hot Zone), die darübergelegt wird

Der Flatten-Pfad folgt exakt diesem Modell. Wenn ein Run ein Href trägt, zeichnet HotPDF zunächst den formatierten Text und erstellt dann eine Link-Annotation über der Box des Runs. Der öffentliche Einstiegspunkt für diese Annotation ist die Seiten-Methode (Page Method) AddURILink, die das /Type /Annot /Subtype /Link-Objekt mit einer /URI-Aktion erstellt und das Annotations-Dictionary zurückgibt. Sein Rechteck ist die gemessene Box des Runs, umgerechnet von den lokalen Koordinaten des Draw-Elements in Seitenkoordinaten. Das Ergebnis ist ein Link, der exakt auf dem Ankertext und nirgendwo sonst landet

// Dieselbe öffentliche API, die der Flatten-Pfad für jeden Anker-Run verwendet. Sie erzeugt
// eine ISO 32000-1 12.5.6.5 Link-Annotation: /Subtype /Link mit einer /URI-Aktion
// über dem angegebenen Rechteck. Die optionale Beschreibung füllt /Contents, sodass ein
// Screenreader das Ziel ankündigen kann.
var
  LinkRect: TRect;
  Annot: THPDFDictionaryObject;
begin
  LinkRect := Rect(72, 690, 268, 706);  // Hit-Box für den Run im Seitenbereich (Page-Space)
  Annot := Pdf.CurrentPage.AddURILink(LinkRect,
    'https://www.example.gov/appeal', 'File an appeal online');
end;

Warum die Trefferzone (Hit Box) aus gemessenen Breiten stammen muss

Es ist verlockend, sich vorzustellen, den Link dadurch zu verorten (locating), dass man die Seite nach seinem sichtbaren Text durchsucht und das Rechteck um alles zeichnet, was gefunden wird. Das funktioniert jedoch nicht, und der Grund dafür ist grundlegend dafür, wie abgeflachter Text (Flattened Text) gespeichert wird. Die formatierten Runs werden mit eingebetteten Subset-Fonts gezeichnet. Ein Subset-Font nummeriert die behaltenen Glyphen neu (renumbers), sodass der Seiteninhalt-Stream (Page Content Stream) hexadezimale CID-Codes enthält, nicht die ursprünglichen Zeichencodes. Die Bytes auf der Seite sind nicht die Buchstaben, die ein Mensch liest, und sie sind nicht als Text durchsuchbar. Eine Suche nach der Beschriftung (Caption) des Ankers findet nichts, da diese Beschriftung nirgendwo im Stream als wörtlicher Text existiert

Der einzig zuverlässige Anker (Anchor) für das Rechteck ist die Geometrie, die der Layout-Durchlauf bereits produziert hat. Der Offset und die gemessene Breite jedes Runs wurden beim Fließen der Zeile berechnet, bevor irgendeine Glyphe neu nummeriert wurde, und sie beschreiben, wo der Text physisch erscheinen wird. HotPDF entnimmt das Link-Rechteck daher direkt aus der platzierten (laid-down) Box des Runs und nicht aus einem Text-Lookup (Suchlauf). Da die Messung den Render-Font verwendete, ist die Box unabhängig vom Subsetting korrekt. Die Geometrie überlebt die Kodierung (Encoding); der Text nicht. Das ist das ganze Argument für die Positionierung anhand gemessener Breiten, und es ist der Grund, warum ein Flattener, der versucht, Links durch Textsuche nachträglich einzubauen (retrofit), Trefferzonen erzeugt, die abdriften (drift) oder verschwinden

Steuern des Abflachens aus Ihrem Code

Für ein PDF, das bereits ein XFA-Paket enthält, ist der Einstiegspunkt FlattenLoadedXFA. Laden Sie das Dokument, rufen Sie die Methode auf und speichern Sie das Ergebnis. Der Parameter Editable entscheidet, was mit den Formularfeldern geschieht: Übergeben Sie True, um sie als ausfüllbare (fillable) AcroForm-Widgets zu behalten, oder False, um jedes Widget als schreibgeschützt (read-only) zu markieren, sodass die Ausgabe ein eingefrorener (frozen) Datensatz ist. Die Rich-Text-Draw-Blöcke, mit ihren formatierten Runs und Link-Annotationen, werden in jedem Fall produziert. Die Funktion gibt die Anzahl der ausgegebenen Widgets zurück

var
  Pdf: THotPDF;
  Emitted, i: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.LoadFromFile('xfa_appeal_form.pdf');
    // True belässt Felder ausfüllbar; False friert sie schreibgeschützt ein.
    Emitted := Pdf.FlattenLoadedXFA(True);

    // Alles, was die Engine nicht abbilden konnte, wird gemeldet, nicht als Ausnahme (Exception) ausgelöst.
    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;

Lesen Sie nach dem Aufruf immer XFAFlattenWarnings. Die Liste wird zu Beginn jedes Flattening-Vorgangs geleert und sammelt eine Zeile für jedes Element, dessen Rendering die Engine abgelehnt hat: eine nicht unterstützte Feldart, ein Draw-Image (Zeichenbild), das sich nicht dekodieren ließ, ein exData-Block ohne nutzbare Spans. Keines davon löst eine Exception aus; somit ist eine leere Warnungsliste Ihr Beleg dafür, dass alles abgebildet (mapped) wurde, und eine nicht-leere sagt Ihnen genau, welche Originale Sie überprüfen müssen. Wenn Sie das rohe XFA als XDP-Bytes vorliegen haben anstatt als geladenes PDF, nimmt die verwandte (sibling) Methode ApplyXFAAsAcroForm diese Bytes direkt entgegen und teilt denselben Code-Pfad und dasselbe Warnungsverhalten. Die komplementäre Methode AddXFAPacket geht in die entgegengesetzte Richtung und bettet ein XFA-Paket in ein Dokument ein, das Sie gerade aufbauen

Überprüfen des Ergebnisses in einem Reader

Öffnen Sie die abgeflachte Datei in Acrobat, oder in einem beliebigen aktuellen Viewer, und prüfen Sie zwei Dinge. Erstens, dass der Rich-Text mit intaktem Styling gerendert wurde: Die fetten Runs sind fett, die farbigen Runs tragen ihre Farbe, und die Spans sitzen in der richtigen Reihenfolge auf der Zeile, anstatt sich zu überlappen oder aus der Box herauszulaufen. Zweitens, dass die Hyperlinks aktiv (live) sind. Fahren Sie mit der Maus über einen Anker (Hover), und die Statusleiste sollte die Zieladresse anzeigen; klicken Sie darauf, und die URI-Aktion sollte diese öffnen. Nutzen Sie den Annotations-Inspektor des Viewers, um zu bestätigen, dass es sich bei jeder einzelnen um eine echte /Link-Annotation handelt, deren /Rect den Ankertext passgenau umschließt (hugs) und über Inhalt liegt, der nun aus einfachen gemalten (painted) Glyphen statt form-gerendertem XFA besteht. Diese Kombination – formatierter statischer Text plus echte Link-Annotationen auf den richtigen Rechtecken – ist es, was das abgeflachte Dokument die XFA-Engines überleben lässt, die es nicht länger benötigt

Das Abflachen der Felder selbst, der Textboxen, Kontrollkästchen und Auswahllisten (Choice Lists), die diesen Rich-Text umgeben, wird in unserem Leitfaden (Walkthrough) zum Abflachen von XFA-Formularen in AcroForm-Widgets behandelt. Für das umfassendere Thema des manuellen Aufbaus und Platzierens von Link-Annotationen, über jene hinaus, die der Flatten-Pfad generiert, lesen Sie Arbeiten mit PDF-Annotationen in HotPDF. Beide bauen auf demselben Annotations- und Formularmodell auf, das mit der HotPDF Component für Delphi und C++Builder ausgeliefert wird