Technischer Artikel

HotPDF-Hyperlinks in Delphi: Tipps zu PrintHyperlink

PDF-Hyperlinks sind URI-Annotationen: ein Rechteck, das einen Seitenbereich abdeckt und beim Anklicken den Viewer anweist, eine URL zu öffnen. Die Annotation und der darunterliegende Text sind völlig unabhängige Objekte. PrintHyperlink von HotPDF bündelt beides in einem Aufruf, zeichnet den Text und berechnet das Annotationsrechteck aus den Metriken des gerenderten Texts. Diese Bequemlichkeit verbirgt ein Detail, das man verstehen sollte, bevor man Produktionscode schreibt. Sie ist auch nicht die ganze Geschichte: AddURILink legt eine anklickbare Fläche über Inhalte, die Sie selbst gezeichnet haben, und AddGoToLink übernimmt die interne Navigation — beides wird weiter unten behandelt

Wie PrintHyperlink funktioniert

PrintHyperlink gehört zu THPDFPage und nimmt vier Argumente entgegen: X- und Y-Koordinaten (in Punkt, Ursprung unten links, Y wächst nach oben), die zu zeichnende Beschriftung und die Ziel-URL. Intern ruft es TextOut in der aktuellen Hyperlink-Farbe auf und berechnet dann sofort das Annotationsrechteck aus TextWidth und TextHeight bei den aktuellen Schriftmetriken. Das bedeutet, dass Schrift und Größe vor dem Aufruf gesetzt sein müssen und sich zwischen dem Zeichnen der Beschriftung und dem Platzieren der Annotation nicht ändern dürfen, weil beides im selben Aufruf aufgelöst wird

Anatomie eines HotPDF-PrintHyperlink-Aufrufs, der zwei unabhängige PDF-Objekte schreibt: die sichtbaren Beschriftungsglyphen, die TextOut zeichnet, und ein URI-Link-Annotationsrechteck, das aus TextWidth und TextHeight berechnet wird
Beschriftungsglyphen und URI-Rechteck sind getrennte PDF-Objekte, weshalb Schrift und Hyperlink-Farbe feststehen müssen, bevor ein Aufruf beide schreibt

Die Standardfarbe ist clBlue. SetRGBHyperlinkColor ändert sie nur für nachfolgende Aufrufe; bereits geschriebene Annotationen werden nicht rückwirkend aktualisiert. Wenn Sie unterschiedliche Farben für verschiedene Link-Gruppen auf derselben Seite benötigen, rufen Sie SetRGBHyperlinkColor vor jeder Gruppe auf und setzen Sie es danach zurück

Hier ein minimales Dokument, das drei Links in zwei verschiedenen Farben schreibt:

procedure CreateLinkedReport(const FileName: string);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;

    Pdf.CurrentPage.SetFont('Arial', [], 11);

    // Standardblau für informative Links
    Pdf.CurrentPage.TextOut(50, 750, 0, 'Reference links:');
    Pdf.CurrentPage.PrintHyperlink(50, 720, 'Product page', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');
    Pdf.CurrentPage.PrintHyperlink(50, 695, 'Online manual', 'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');

    // Rot für den Aktions-Link
    Pdf.CurrentPage.SetRGBHyperlinkColor(clRed);
    Pdf.CurrentPage.PrintHyperlink(50, 660, 'Purchase license', 'https://www.loslab.com/en-us/buy-hotpdf-fastspring.html');
    Pdf.CurrentPage.SetRGBHyperlinkColor(clBlue);  // Standard wiederherstellen

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Die Koordinatenfalle

HotPDF verwendet einen Ursprung unten links mit nach oben wachsendem Y, in Punkt (1/72 Zoll). Eine A4-Seite misst 595 x 842 pt; eine US-Letter-Seite 612 x 792 pt. Y=750 liegt nahe dem oberen Rand einer A4-Seite, und Y=50 läge nahe dem unteren Rand. Wer von Bildschirmgrafik oder HTML kommt, nimmt das Gegenteil an und platziert die erste Link-Zeile direkt außerhalb des sichtbaren Bereichs

Das Annotationsrechteck, das PrintHyperlink berechnet, verwendet dasselbe Koordinatensystem. Wenn Sie die Seite später drehen, skalieren oder die Seitengröße ändern, ohne Ihre X/Y-Werte neu zu berechnen, driften der sichtbare Text und das anklickbare Rechteck auseinander. Der Link „funktioniert“ in dem Sinne, dass ein Klick irgendwo in der Nähe des Texts die URL auslöst, aber die aktive Zone passt nicht mehr zu dem, was der Leser sieht. Testen Sie auf der tatsächlichen Seitengröße und Zoomstufe, die Sie ausliefern, nicht nur auf der Entwicklungsmaschine bei 100 %

Ein Fall, in dem die Drift garantiert ist: Wenn Sie PrintHyperlink mit Koordinaten aufrufen, die zu einer A4-Seite passen, und dann zu einer benutzerdefinierten schmalen Seite wechseln, ohne die X/Y-Werte anzupassen, kann die Annotation komplett außerhalb der Seite landen. Das Annotationsobjekt wird trotzdem in das PDF geschrieben; die meisten Viewer schneiden es stillschweigend ab, sodass der Link einfach ohne jede Fehlermeldung verschwindet

Beschriftungstext versus URL-Ziel

Die Argumente Text und Link sind unabhängig. Sie können „Download invoice PDF“ zeichnen, während das Ziel eine vollqualifizierte HTTPS-URL mit Query-Parametern ist. Diese Trennung ist gewollt; die sichtbare Beschriftung sollte für Menschen lesbar sein, und die URL kann lang sein oder dynamisch erzeugt werden

Probleme entstehen, wenn die Beschriftung die rohe URL selbst ist, besonders eine lange. Wenn die URL visuell über zwei Zeilen umbricht, das Annotationsrechteck aber für eine einzeilige Zeichenkette berechnet wurde, ist nur die erste Zeile anklickbar. PrintHyperlink behandelt keinen mehrzeiligen Textfluss; halten Sie die Beschriftung kurz genug, um bei der aktuellen Schriftgröße und Seitenbreite in eine Zeile zu passen, verwenden Sie eine kurze beschreibende Beschriftung mit der vollständigen URL als Ziel, oder wenden Sie den im nächsten Abschnitt gezeigten zeilenweisen Workaround an

Für Dokumente, die archiviert oder ohne aktive Internetverbindung verteilt werden, sollten Sie außerdem überlegen, ob die URL selbst irgendwo im Dokumenttext in gedruckter Form erscheinen sollte, nicht nur als Annotationsmetadaten. Ein Leser, der das PDF auf Papier druckt, hat von einer URI-Annotation nichts

Die mehrzeilige Einschränkung umgehen

Wenn eine Link-Beschriftung wirklich mehr als eine Zeile umfassen muss — eine lange, wörtlich gedruckte URL oder ein umbrochener Satz, der von Anfang bis Ende anklickbar sein soll —, besteht die Lösung darin, ihn nicht mehr als einen Link zu behandeln, sondern als einen Link pro Zeile. Jeder PrintHyperlink-Aufruf berechnet sein Rechteck aus dem Text, den er zeichnet, sodass mehrere Aufrufe mit demselben Link-Ziel mehrere korrekt dimensionierte Annotationen erzeugen, die alle dieselbe URL öffnen. Der Leser bemerkt keinen Unterschied; jede Zeile reagiert auf einen Klick

Vergleich einer umbrochenen HotPDF-Hyperlink-Beschriftung, die eine einzige Annotation nur über ihrer ersten Zeile erhält, mit einem PrintHyperlink-Aufruf pro gerenderter Zeile, die sich dasselbe URL-Ziel teilen
Ein für eine Zeile berechnetes Rechteck lässt jede umbrochene Fortsetzung stranden, während zeilenweise Aufrufe ein Ziel teilen und den ganzen Block anklickbar halten
procedure PrintWrappedHyperlink(Page: THPDFPage; X, TopY, LineStep: Single;
  const Lines: array of AnsiString; const Link: AnsiString);
var
  I: Integer;
begin
  for I := 0 to High(Lines) do
    Page.PrintHyperlink(X, TopY - I * LineStep, Lines[I], Link);
end;

// Verwendung: die Beschriftung an den Stellen trennen, an denen Ihr Layout sie umbricht
Pdf.CurrentPage.SetFont('Arial', [], 10);
PrintWrappedHyperlink(Pdf.CurrentPage, 50, 400, 14,
  ['https://www.loslab.com/en-us/pdf-library/',
   'delphi-pdf-component.html'],
  'https://www.loslab.com/en-us/pdf-library/delphi-pdf-component.html');

Das Aufteilen der Zeichenkette liegt in Ihrer Verantwortung: Trennen Sie sie an denselben Stellen, an denen sie bei der aktuellen Schrift und Spaltenbreite visuell umbrechen würde, und prüfen Sie jede Kandidatenzeile mit TextWidth. Die Alternative besteht darin, den umbrochenen Text selbst mit einfachen TextOut-Aufrufen zu zeichnen und dann ein AddURILink-Rechteck über jede Zeile zu legen — der bessere Weg, wenn der Text bereits von Ihrer eigenen Zeilenumbruchlogik erzeugt wird, was uns zu dieser Funktion bringt

AddURILink: anklickbare Flächen über allem, was Sie gezeichnet haben

PrintHyperlink ist ein Komfort-Wrapper: Er zeichnet seine eigene Beschriftung und leitet das Rechteck aus deren Metriken ab. AddURILink ist die direkt freigelegte untere Hälfte:

function AddURILink(Rectangle: TRect; const URL: AnsiString;
  const Description: AnsiString = ''): THPDFDictionaryObject;

Es schreibt nur die Annotation — es wird kein Text gezeichnet und keine Farbe geändert. Das Rectangle wird im selben Koordinatenraum wie Ihre Zeichenaufrufe interpretiert, sodass Sie exakt die X/Y-Werte wiederverwenden können, die Sie an TextOut oder einen Bildaufruf übergeben haben. Das macht es zum richtigen Werkzeug, wann immer der sichtbare Inhalt bereits existiert: ein Bild-Hotspot, eine Tabellenzelle, ein zuvor gezeichneter Textblock oder eine Zeile eines umbrochenen Absatzes wie im Workaround oben. Die Annotation trägt einen Rahmen der Breite null, sodass sich sichtbar nichts ändert; der anklickbare Bereich ist exakt das von Ihnen angegebene Rechteck

Die Funktion gibt das Annotations-Dictionary als THPDFDictionaryObject zurück. Die meisten Aufrufer verwerfen das Ergebnis, aber wer es behält, kann die Einträge der Annotation anpassen, bevor das Dokument geschrieben wird

Zwei Konformitätsdetails sind eingebaut. In PDF/A-Modi wird das Druck-Flag der Annotation so gesetzt, wie diese Standards es verlangen. Unter PDFUACompliance muss der Parameter Description eine nicht-leere Zeichenkette sein — er wird zum /Contents-Eintrag der Annotation, den assistive Technologie für den Link vorliest —, und der Aufruf löst eine Exception aus, statt stillschweigend eine nicht konforme Datei zu erzeugen. PrintHyperlink ist älter als diese Regel und hängt keine Beschreibung an; für PDF/UA-Ausgabe zeichnen Sie die Beschriftung daher mit TextOut und platzieren die Annotation mit AddURILink plus einer aussagekräftigen Beschreibung

Die Entscheidungsregel ist einfach: Verwenden Sie PrintHyperlink, wenn der Link ein kurzes Textstück ist, das Sie noch nicht gezeichnet haben; verwenden Sie AddURILink, wenn der anklickbare Bereich durch Inhalte definiert ist, die Sie selbst zeichnen oder vermessen

Interne Navigation mit AddGoToLink

Externe URLs sind nur die Hälfte dessen, was Link-Annotationen leisten. Die andere Hälfte ist die Navigation innerhalb des Dokuments — ein Inhaltsverzeichnis, das zu Kapiteln springt, Querverweise zwischen Abschnitten. HotPDF stellt dies über AddGoToLink bereit:

procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
  YPos: Single = -1; const Description: AnsiString = '');

Drei Bedeutungen verdienen eine präzise Erklärung, da sich keine aus der Signatur erraten lässt. TargetPageIndex ist nullbasiert: Die erste Seite des Dokuments ist Seite 0, passend zu CurrentPageNumber. Die Zielseite muss beim Aufruf bereits existieren; liegt der Index außerhalb des Bereichs, kehrt die Prozedur zurück, ohne eine Annotation hinzuzufügen — keine Exception, kein Link, keine Warnung. Für ein Inhaltsverzeichnis, das nach vorn verweist, erstellen Sie zuerst alle Seiten, wechseln dann zurück und fügen die Links hinzu

YPos wählt die vertikale Position auf der Zielseite, im selben Koordinatenraum wie Ihre Zeichenaufrufe. Der Standardwert -1 (jeder negative Wert) schreibt eine Null-Zielkoordinate und weist den Viewer an, seine aktuelle vertikale Position beizubehalten, wenn er auf der Zielseite landet. Übergeben Sie einen nicht-negativen Wert, scrollt der Viewer so, dass diese Position am oberen Fensterrand sitzt — verwenden Sie die Y-Koordinate der Überschrift, auf die Sie verlinken. Der Zoom bleibt immer unverändert. Wie bei AddURILink muss Description unter PDFUACompliance nicht-leer sein und wird zum Alternativtext des Links

HotPDF: Verlinktes Inhaltsverzeichnis, mit AddGoToLink erstellt, das nullbasierte TargetPageIndex-Sprünge von der Inhaltsseite zu Kapitelseiten zeigt, wobei jede Überschrift am oberen Fensterrand landet
Rechtecke reichen über den Text hinaus, damit ganze Zeilen reagieren, und ein festes Lande-Y platziert jede Kapitelüberschrift am oberen Fensterrand
procedure BuildLinkedTOC(const FileName: string);
const
  Chapters: array[0..2] of string =
    ('Introduction', 'Installation', 'API Reference');
var
  Pdf: THotPDF;
  I, Y: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;                        // Seite 0 wird zur Inhaltsverzeichnisseite

    // Zuerst die Kapitelseiten erstellen, damit die Link-Ziele existieren
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // Seiten 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Zurück zu Seite 0 wechseln und die Einträge mit ihren Links zeichnen
    Pdf.CurrentPageNumber := 0;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Contents');
    Pdf.CurrentPage.SetFont('Arial', [], 11);

    Y := 720;
    for I := 0 to High(Chapters) do
    begin
      Pdf.CurrentPage.TextOut(70, Y, 0, Chapters[I]);
      Pdf.CurrentPage.AddGoToLink(
        Rect(70, Y + 14, 300, Y - 3),    // deckt den Eintrag mit Abstand ab
        I + 1,                           // nullbasiert: Kapitel sind Seiten 1..3
        780,                             // mit der Überschrift am oberen Rand landen
        AnsiString('Go to ' + Chapters[I]));
      Y := Y - 25;
    end;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Jeder Eintrag erhält ein Rechteck, das breiter als der Text ist, damit die ganze Zeile auf den Zeiger reagiert, und jeder Link landet mit der Kapitelüberschrift (gezeichnet bei Y=780) am oberen Fensterrand. Wenn Sie später eine Seite vor den Kapiteln einfügen, verschiebt sich jeder TargetPageIndex um eins; berechnen Sie die Indizes aus Ihrer Seitenerstellungsschleife, statt sie fest zu codieren

Ein vollständiges Beispiel zur Dokumenterzeugung

Das folgende Muster zeigt ein realistischeres Szenario: die Erzeugung eines kurzen Berichts mit Kopfbereich, Fließtext und einer Fußzeile mit Links, alles aus Code statt aus einem Formular mit TEdit-Feldern:

procedure GenerateProductSheet(
  const FileName, ProductName, ProductURL, SupportURL: string);
var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.Compression := cmFlateDecode;
    Pdf.BeginDoc;

    // Kopfbereich
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));

    // Platzhalter für den Textabsatz
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Fußzeilen-Links
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 80, 0, 'Links:');
    Pdf.CurrentPage.PrintHyperlink(50, 60, 'Product page', ProductURL);
    Pdf.CurrentPage.PrintHyperlink(200, 60, 'Support', SupportURL);

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

Beachten Sie, dass SetFont vor jeder Gruppe von Textaufrufen aufgerufen wird. Die Schrift bleibt über AddPage hinweg nicht erhalten, und wenn Sie vergessen, sie auf einer neuen Seite vor PrintHyperlink zu setzen, wird das Annotationsrechteck gegen die Standardmetriken der Seite berechnet, was von Ihren Erwartungen abweichen kann

Wo sich die Annotationsbehandlung zwischen Viewern unterscheidet

PDF-URI-Annotationen sind in ISO 32000-1 §12.6.4.7 definiert, und jeder konforme Viewer sollte sich daran halten. In der Praxis unterscheiden sich einige Verhaltensweisen je nach Viewer. Adobe Acrobat zeigt beim ersten Klick eine Sicherheitsabfrage für URLs, die nicht in der Liste vertrauenswürdiger Domains stehen; viele Browser und leichtgewichtige Reader tun das nicht. Manche Unternehmens-PDF-Viewer in abgeschotteten Umgebungen deaktivieren URI-Annotationen per Richtlinie vollständig, sodass ein Klick nichts bewirkt, ohne sichtbaren Fehler. Mobile PDF-Apps unterscheiden sich darin, ob sie Links in der In-App-Webansicht öffnen oder an den Systembrowser übergeben

Nichts davon sind Fehler, die Sie von der Erzeugungsseite aus beheben können; es sind Richtlinienentscheidungen der Viewer. Was Sie tun können, ist Link-Beschriftungen zu schreiben, die die URL auch im Dokumenttext sichtbar machen, damit ein Leser in einer eingeschränkten Umgebung die Adresse trotzdem manuell kopieren kann. Die Annotation ist die Bequemlichkeit; der Text ist der Fallback

Ein weiteres Detail, das man kennen sollte: PDF-URI-Annotationen tragen standardmäßig keine sichtbare Unterstreichung. Die Unterstreichung, die Sie in den meisten Viewern sehen, zeichnet der Viewer selbst anhand des Annotationstyps, nicht eine Glyphe im Content-Stream. Wenn Sie eine physische Unterstreichung benötigen, die das Drucken über einen nicht-interaktiven Renderer oder die PDF-zu-Bild-Konvertierung übersteht, zeichnen Sie sie explizit mit LineTo und Stroke beim passenden Y-Versatz unterhalb der Textgrundlinie. Das ist eine separate Zeichenoperation, nichts, was PrintHyperlink für Sie übernimmt

Die hier gezeigte Hyperlink-API ist Teil der HotPDF Delphi Component für Delphi und C++Builder