Les liens hypertexte PDF sont des annotations URI : un rectangle couvrant une zone de la page qui, lorsqu'on clique dessus, indique à la visionneuse d'ouvrir une URL. L'annotation et le texte en dessous sont des objets complètement indépendants. PrintHyperlink de HotPDF regroupe les deux en un seul appel, dessinant le texte et calculant le rectangle d'annotation à partir des métriques de texte rendues. Cette commodité cache un détail qui vaut la peine d'être compris avant d'écrire du code de production. Ce n'est pas non plus toute l'histoire : AddURILink place une zone cliquable sur le contenu que vous avez dessiné vous-même, et AddGoToLink gère la navigation interne — les deux sont abordés ci-dessous
Comment fonctionne PrintHyperlink
PrintHyperlink se trouve sur THPDFPage et prend quatre arguments : les coordonnées X et Y (en points, origine en bas à gauche, Y augmentant vers le haut), la chaîne d'étiquette (label string) à dessiner et la cible de l'URL. En interne, il appelle TextOut dans la couleur de lien hypertexte actuelle, puis calcule immédiatement le rectangle d'annotation à partir de TextWidth et TextHeight avec les métriques de police actuelles. Cela signifie que la police et la taille doivent être définies avant l'appel, et elles ne doivent pas changer entre le dessin de l'étiquette et le placement de l'annotation, car les deux sont résolus dans le même appel
La couleur par défaut est clBlue. SetRGBHyperlinkColor ne la modifie que pour les appels ultérieurs ; il ne met pas à jour rétroactivement les annotations déjà écrites. Si vous avez besoin de couleurs différentes pour différents groupes de liens sur la même page, appelez SetRGBHyperlinkColor avant chaque groupe et réinitialisez-la ensuite
Voici un document minimal qui écrit trois liens avec deux couleurs différentes :
procedure CreateLinkedReport(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
// Default blue for informational 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');
// Red for the action 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); // restore default
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Le piège des coordonnées
HotPDF utilise une origine en bas à gauche avec Y croissant vers le haut, en points (1/72 de pouce). Une page A4 mesure 595 x 842 pt ; une page Lettre US mesure 612 x 792 pt. Y = 750 se trouve près du haut d'une page A4, et Y = 50 serait près de la marge inférieure. Quiconque vient des graphiques d'écran ou du HTML suppose le contraire et place la première ligne de lien directement hors de la zone visible
Le rectangle d'annotation que calcule PrintHyperlink utilise le même système de coordonnées. Si vous faites pivoter la page ultérieurement, la mettez à l'échelle ou modifiez la taille de la page sans recalculer vos valeurs X/Y, le texte visible et le rectangle cliquable s'écarteront l'un de l'autre. Le lien « fonctionne » dans le sens où un clic à proximité du texte déclenche l'URL, mais la zone réactive ne correspond plus à ce que le lecteur voit. Testez sur la taille de page et le niveau de zoom réels que vous livrez, pas seulement sur la machine de développement à 100 %
Un cas où la dérive (drift) est garantie : si vous appelez PrintHyperlink avec des coordonnées appropriées pour une page A4, puis passez à une page au format étroit personnalisé sans ajuster les valeurs X/Y, l'annotation peut se retrouver entièrement en dehors de la page. L'objet d'annotation est toujours écrit dans le PDF ; la plupart des visionneuses le découpent (clip) silencieusement, de sorte que le lien disparaît simplement sans aucune erreur
Texte de l'étiquette par rapport à la cible de l'URL
Les arguments Text et Link sont indépendants. Vous pouvez dessiner « Download invoice PDF » (Télécharger le PDF de la facture) alors que la cible est une URL HTTPS complète (fully qualified) avec des paramètres de requête. Cette séparation est délibérée ; l'étiquette visible doit être lisible par l'homme et l'URL peut être longue ou générée de manière dynamique
Ce qui pose problème, c'est lorsque l'étiquette est l'URL brute elle-même, surtout si elle est longue. Si l'URL s'enroule visuellement sur deux lignes mais que le rectangle d'annotation a été calculé pour une chaîne d'une seule ligne, seule la première ligne est cliquable. PrintHyperlink ne gère pas le flux multiligne ; gardez l'étiquette suffisamment courte pour tenir sur une ligne avec la taille de police et la largeur de page actuelles, utilisez une courte étiquette descriptive avec l'URL complète comme cible, ou appliquez la solution de contournement par ligne présentée dans la section suivante
Pour les documents qui seront archivés ou distribués sans connexion Internet active, déterminez également si l'URL elle-même doit apparaître sous forme imprimée quelque part dans le corps du document, et pas seulement sous forme de métadonnées d'annotation. Un lecteur qui imprime le PDF sur papier ne tire aucun bénéfice d'une annotation URI
Contourner la limitation multiligne
Lorsqu'une étiquette de lien doit véritablement s'étendre sur plus d'une ligne (une longue URL imprimée textuellement ou une phrase renvoyée à la ligne qui doit être cliquable de bout en bout), la solution consiste à cesser de la traiter comme un seul lien et à la traiter comme un lien par ligne. Chaque appel à PrintHyperlink calcule son rectangle à partir du texte qu'il dessine, de sorte que plusieurs appels partageant la même cible Link produisent plusieurs annotations de taille correcte qui ouvrent toutes la même URL. Le lecteur ne peut pas voir la différence ; chaque ligne répond à un clic
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;
// Usage: break the label at the positions where your layout wraps it
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');
Le fractionnement de la chaîne est de votre responsabilité : cassez-la (break it) aux mêmes positions où elle s'enroulerait visuellement avec la police et la largeur de colonne actuelles, en utilisant TextWidth pour tester chaque ligne candidate. L'alternative consiste à dessiner vous-même le texte renvoyé à la ligne avec de simples appels TextOut, puis à superposer un rectangle AddURILink sur chaque ligne — la meilleure voie lorsque le texte est déjà produit par votre propre logique de retour à la ligne (word-wrap logic), ce qui nous amène à cette fonction
AddURILink : zones cliquables sur tout ce que vous avez dessiné
PrintHyperlink est un wrapper de commodité : il dessine sa propre étiquette et dérive le rectangle des métriques de cette étiquette. AddURILink est la moitié de niveau inférieur exposée directement :
function AddURILink(Rectangle: TRect; const URL: AnsiString;
const Description: AnsiString = ''): THPDFDictionaryObject;
Il n'écrit que l'annotation : aucun texte n'est dessiné et aucune couleur ne change. Le Rectangle est interprété dans le même espace de coordonnées que vos appels de dessin, vous pouvez donc réutiliser les valeurs X/Y exactes que vous avez passées à TextOut ou à un appel d'image. Cela en fait le bon outil chaque fois que le contenu visible existe déjà : une zone réactive (hotspot) d'image, une cellule de tableau, un bloc de texte dessiné plus tôt ou une ligne d'un paragraphe renvoyé à la ligne comme dans la solution de contournement ci-dessus. L'annotation porte une bordure de largeur nulle (zero-width border), donc rien de visible ne change ; la région cliquable est exactement le rectangle que vous spécifiez
La fonction renvoie le dictionnaire d'annotation sous forme de THPDFDictionaryObject. La plupart des appelants ignorent le résultat, mais le conserver vous permet d'ajuster les entrées de l'annotation avant l'écriture du document
Deux détails de conformité sont intégrés. Dans les modes PDF/A, le drapeau d'impression de l'annotation est défini comme l'exigent ces normes. Sous PDFUACompliance, le paramètre Description doit être une chaîne non vide — il devient l'entrée /Contents de l'annotation, qui est ce que la technologie d'assistance annonce pour le lien — et l'appel lève une exception plutôt que d'émettre silencieusement un fichier non conforme. PrintHyperlink est antérieur à cette règle et n'attache aucune description, donc pour la sortie PDF/UA, dessinez l'étiquette avec TextOut et placez l'annotation avec AddURILink plus une description significative
La règle de décision est simple : utilisez PrintHyperlink lorsque le lien est un court morceau de texte que vous n'avez pas encore dessiné ; utilisez AddURILink lorsque la région cliquable est définie par un contenu que vous dessinez ou mesurez vous-même
Navigation interne avec AddGoToLink
Les URL externes ne représentent que la moitié de ce que font les annotations de lien. L'autre moitié est la navigation à l'intérieur du document : une table des matières qui saute aux chapitres, des références croisées entre les sections. HotPDF expose cela via AddGoToLink :
procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
YPos: Single = -1; const Description: AnsiString = '');
Il convient de préciser trois sémantiques, car aucune n'est devinable à partir de la signature. TargetPageIndex est de base zéro (zero-based) : la première page du document est la page 0, correspondant à CurrentPageNumber. La page cible doit déjà exister lorsque vous effectuez l'appel ; si l'index est hors limites, la procédure revient sans ajouter d'annotation — pas d'exception, pas de lien, pas d'avertissement. Pour une table des matières qui pointe vers l'avant, créez d'abord toutes les pages, puis revenez en arrière et ajoutez les liens
YPos sélectionne la position verticale sur la page cible, dans le même espace de coordonnées que vos appels de dessin. La valeur par défaut de -1 (toute valeur négative) écrit une coordonnée de destination nulle, indiquant à la visionneuse de conserver sa position verticale actuelle lorsqu'elle atterrit sur la page cible. Passez une valeur non négative et la visionneuse fait défiler le document de sorte que cette position se trouve en haut de la fenêtre — utilisez la coordonnée Y de l'en-tête vers lequel vous créez le lien. Le zoom est toujours laissé inchangé. Comme pour AddURILink, Description doit être non vide sous PDFUACompliance et devient le texte alternatif du lien
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; // page 0 becomes the TOC page
// Create the chapter pages first so the link targets exist
for I := 0 to High(Chapters) do
begin
Pdf.AddPage; // pages 1..3
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
end;
// Switch back to page 0 and draw the TOC entries with their links
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), // covers the entry with padding
I + 1, // zero-based: chapters are pages 1..3
780, // land with the heading at the top
AnsiString('Go to ' + Chapters[I]));
Y := Y - 25;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Chaque entrée obtient un rectangle plus large que le texte afin que toute la ligne réponde au pointeur, et chaque lien atterrit avec l'en-tête du chapitre (dessiné à Y = 780) en haut de la fenêtre. Si vous insérez ultérieurement une page avant les chapitres, chaque TargetPageIndex se décale de un ; calculez les index à partir de votre boucle de création de page plutôt que de les coder en dur (hard-coding)
Un exemple complet de génération de documents
Le modèle ci-dessous montre un scénario plus réaliste : générer un court rapport avec une section d'en-tête, un corps de texte et une ligne de liens en pied de page, le tout à partir du code plutôt qu'à partir d'un formulaire avec des champs TEdit :
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;
// Header
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));
// Body paragraph placeholder
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');
// Footer 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;
Notez que SetFont est appelé avant chaque groupe d'appels de texte. La police ne persiste pas à travers AddPage, et si vous oubliez de la définir avant PrintHyperlink sur une nouvelle page, le rectangle d'annotation sera calculé en fonction des métriques par défaut de la page, qui peuvent différer de ce à quoi vous vous attendez
Où la gestion des annotations varie selon les visionneuses
Les annotations URI PDF sont définies dans l'ISO 32000-1 §12.6.4.7, et toute visionneuse conforme doit les suivre. En pratique, quelques comportements diffèrent selon la visionneuse. Adobe Acrobat affiche une invite de sécurité au premier clic pour les URL qui ne figurent pas dans la liste des domaines de confiance ; de nombreux navigateurs et lecteurs légers (lightweight readers) ne le font pas. Certaines visionneuses de PDF d'entreprise dans des environnements verrouillés désactivent complètement les annotations URI par politique, de sorte qu'un clic ne fait rien, sans erreur visible. Les applications PDF mobiles varient selon qu'elles ouvrent les liens à l'intérieur de la vue Web de l'application ou qu'elles les transmettent au navigateur système
Aucun d'entre eux n'est un bogue que vous pouvez corriger du côté de la génération ; ce sont des décisions de politique de la visionneuse. Ce que vous pouvez faire, c'est écrire des étiquettes de lien qui rendent l'URL visible dans le corps du document également, afin qu'un lecteur dans un environnement restreint puisse toujours copier l'adresse manuellement. L'annotation est la commodité ; le texte est la solution de repli (fallback)
Un autre détail qu'il vaut la peine de connaître : les annotations URI PDF ne comportent aucun soulignement visuel par défaut. Le soulignement que vous voyez dans la plupart des visionneuses est dessiné par la visionneuse elle-même en fonction du type d'annotation, et non par un glyphe dans le flux de contenu. Si vous avez besoin d'un soulignement physique qui survit à l'impression vers un dispositif de rendu non interactif (non-interactive renderer) ou à la conversion PDF en image, dessinez-le explicitement avec LineTo et Stroke au décalage Y approprié sous la ligne de base du texte. C'est une opération de dessin distincte, et non quelque chose que PrintHyperlink gère pour vous
L'API de lien hypertexte présentée ici fait partie du Composant HotPDF pour Delphi et C++Builder