Articolo tecnico

Link ipertestuali in Delphi con HotPDF PrintHyperlink

I link ipertestuali del PDF sono annotazioni URI: un rettangolo che copre una porzione di pagina e che, al clic, dice al visualizzatore di aprire un URL. L'annotazione e il testo che le sta sotto sono oggetti del tutto indipendenti. Il metodo PrintHyperlink di HotPDF racchiude entrambi in una sola chiamata, disegnando il testo e calcolando il rettangolo dell'annotazione dalle metriche del testo disegnato. Quella comodità nasconde un dettaglio che vale la pena capire prima di scrivere codice di produzione. E non è nemmeno tutta la storia: AddURILink colloca un'area cliccabile sopra contenuto che avete disegnato voi, e AddGoToLink gestisce la navigazione interna — entrambi trattati più sotto

Come funziona PrintHyperlink

PrintHyperlink vive su THPDFPage e riceve quattro argomenti: le coordinate X e Y (in punti, origine in basso a sinistra, Y crescente verso l'alto), la stringa dell'etichetta da disegnare e l'URL di destinazione. Internamente chiama TextOut nel colore corrente dei collegamenti, poi calcola subito il rettangolo dell'annotazione da TextWidth e TextHeight con le metriche del font corrente. Ciò significa che font e corpo vanno impostati prima della chiamata, e non devono cambiare fra il disegno dell'etichetta e il posizionamento dell'annotazione, perché entrambi si risolvono nella stessa chiamata

Anatomia di una sola chiamata PrintHyperlink di HotPDF che scrive due oggetti PDF indipendenti: i glifi visibili dell etichetta disegnati da TextOut e il rettangolo di annotazione del link URI calcolato da TextWidth e TextHeight
I glifi dell'etichetta e il rettangolo URI sono oggetti PDF distinti, ed è per questo che font e colore del collegamento devono essere stabili prima che una sola chiamata scriva entrambi

Il colore predefinito è clBlue. SetRGBHyperlinkColor lo cambia solo per le chiamate successive; non aggiorna retroattivamente le annotazioni già scritte. Se vi servono colori diversi per gruppi di link diversi sulla stessa pagina, chiamate SetRGBHyperlinkColor prima di ogni gruppo e riportatelo indietro dopo

Ecco un documento minimo che scrive tre link con due colori diversi:

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

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

    // Blu predefinito per i link informativi
    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');

    // Rosso per il link di azione
    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);  // ripristina il predefinito

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

La trappola delle coordinate

HotPDF usa un'origine in basso a sinistra con la Y che cresce verso l'alto, in punti (1/72 di pollice). Una pagina A4 misura 595 x 842 pt; una US Letter misura 612 x 792 pt. Y=750 sta vicino alla sommità di una pagina A4, e Y=50 sarebbe vicino al margine inferiore. Chi arriva dalla grafica a schermo o dall'HTML presuppone l'opposto e piazza la prima riga di link fuori dall'area visibile

Il rettangolo di annotazione che PrintHyperlink calcola usa lo stesso sistema di coordinate. Se in seguito ruotate la pagina, la scalate o ne cambiate il formato senza ricalcolare i valori X/Y, il testo visibile e il rettangolo cliccabile si allontanano fra loro. Il link «funziona» nel senso che cliccando da qualche parte vicino al testo si apre l'URL, ma la zona sensibile non corrisponde più a ciò che il lettore vede. Collaudate sul formato pagina e sul livello di zoom che consegnate davvero, non solo sulla macchina di sviluppo al 100%

Un caso in cui lo scostamento è garantito: se chiamate PrintHyperlink con coordinate adatte a una pagina A4 e poi passate a un formato personalizzato stretto senza regolare i valori X/Y, l'annotazione può finire del tutto fuori pagina. L'oggetto annotazione viene comunque scritto nel PDF; la maggior parte dei visualizzatori lo taglia in silenzio, quindi il link semplicemente sparisce senza alcun errore

Testo dell'etichetta contro URL di destinazione

Gli argomenti Text e Link sono indipendenti. Potete disegnare «Scarica la fattura in PDF» mentre la destinazione è un URL HTTPS completo con parametri di query. Quella separazione è voluta; l'etichetta visibile dovrebbe essere leggibile da una persona e l'URL può essere lungo o generato dinamicamente

I problemi nascono quando l'etichetta è l'URL grezzo, soprattutto se lungo. Se l'URL va a capo visivamente su due righe ma il rettangolo di annotazione è stato calcolato per una stringa su una riga sola, soltanto la prima riga è cliccabile. PrintHyperlink non gestisce il flusso su più righe; tenete l'etichetta abbastanza corta da stare su una riga con il corpo del font e la larghezza di pagina correnti, usate una etichetta breve e descrittiva con l'URL completo come destinazione, oppure applicate la soluzione per riga mostrata nella sezione successiva

Per i documenti che verranno archiviati o distribuiti senza una connessione internet attiva, valutate anche se l'URL stesso debba comparire in forma stampata da qualche parte nel corpo del documento, non solo come metadato dell'annotazione. Un lettore che stampa il PDF su carta non ricava nulla da una annotazione URI

Aggirare il limite delle righe multiple

Quando l'etichetta di un link deve davvero occupare più di una riga — un URL lungo stampato per esteso, o una frase mandata a capo che deve essere cliccabile da un capo all'altro — la soluzione è smettere di trattarla come un solo link e trattarla come un link per riga. Ogni chiamata a PrintHyperlink calcola il proprio rettangolo dal testo che disegna, quindi più chiamate che condividono la stessa destinazione Link producono più annotazioni di dimensione corretta che aprono tutte lo stesso URL. Il lettore non nota la differenza; ogni riga risponde al clic

Confronto fra una etichetta di link HotPDF mandata a capo che riceve una sola annotazione a copertura della prima riga e una chiamata PrintHyperlink per ciascuna riga disegnata che condivide la stessa destinazione URL
Un rettangolo calcolato per una riga lascia scoperte tutte le continuazioni, mentre le chiamate per riga condividono una destinazione e mantengono cliccabile l'intero blocco
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;

// Uso: spezzate l'etichetta nei punti in cui il vostro layout la manda a capo
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');

Spezzare la stringa è responsabilità vostra: interrompetela negli stessi punti in cui andrebbe a capo visivamente con il font e la larghezza di colonna correnti, usando TextWidth per provare ogni riga candidata. L'alternativa è disegnare voi stessi il testo mandato a capo con semplici chiamate TextOut e poi sovrapporre un rettangolo AddURILink a ciascuna riga — la via migliore quando il testo è già prodotto dalla vostra logica di ritorno a capo, il che ci porta a quella funzione

AddURILink: aree cliccabili sopra qualunque cosa abbiate disegnato

PrintHyperlink è un involucro di comodo: disegna la propria etichetta e ricava il rettangolo dalle metriche di quella etichetta. AddURILink è la metà di livello più basso esposta direttamente:

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

Scrive soltanto l'annotazione: non viene disegnato alcun testo e nessun colore cambia. Il Rectangle è interpretato nello stesso spazio di coordinate delle vostre chiamate di disegno, quindi potete riusare esattamente i valori X/Y passati a TextOut o a una chiamata di immagine. Ciò la rende lo strumento giusto ogni volta che il contenuto visibile esiste già: un'area sensibile su una immagine, una cella di tabella, un blocco di testo disegnato prima, o una riga di un paragrafo mandato a capo come nella soluzione qui sopra. L'annotazione porta un bordo di larghezza zero, quindi nulla di visibile cambia; la regione cliccabile è esattamente il rettangolo che indicate

La funzione restituisce il dizionario dell'annotazione come THPDFDictionaryObject. La maggior parte dei chiamanti scarta il risultato, ma tenerlo vi permette di regolare le voci dell'annotazione prima che il documento venga scritto

Due dettagli di conformità sono integrati. Nelle modalità PDF/A il flag di stampa dell'annotazione viene impostato come quegli standard richiedono. Sotto PDFUACompliance il parametro Description deve essere una stringa non vuota — diventa la voce /Contents dell'annotazione, cioè ciò che le tecnologie assistive annunciano per il link — e la chiamata solleva una eccezione invece di emettere in silenzio un file non conforme. PrintHyperlink precede quella regola e non allega alcuna descrizione, quindi per l'uscita PDF/UA disegnate l'etichetta con TextOut e collocate l'annotazione con AddURILink più una descrizione significativa

La regola di scelta è semplice: usate PrintHyperlink quando il link è un breve testo che non avete ancora disegnato; usate AddURILink quando la regione cliccabile è definita da contenuto che disegnate o misurate voi

Navigazione interna con AddGoToLink

Gli URL esterni sono solo metà di ciò che fanno le annotazioni di link. L'altra metà è la navigazione dentro il documento — un indice che salta ai capitoli, riferimenti incrociati fra sezioni. HotPDF la espone tramite AddGoToLink:

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

Tre semantiche vanno enunciate con precisione, dato che nessuna è intuibile dalla firma. TargetPageIndex parte da zero: la prima pagina del documento è la pagina 0, in accordo con CurrentPageNumber. La pagina di destinazione deve già esistere quando fate la chiamata; se l'indice è fuori intervallo, la procedura ritorna senza aggiungere alcuna annotazione: nessuna eccezione, nessun link, nessun avviso. Per un indice che punta in avanti, create prima tutte le pagine, poi tornate indietro e aggiungete i link

YPos seleziona la posizione verticale sulla pagina di destinazione, nello stesso spazio di coordinate delle vostre chiamate di disegno. Il valore predefinito -1 (qualunque valore negativo) scrive una coordinata di destinazione nulla, dicendo al visualizzatore di mantenere la posizione verticale corrente quando atterra sulla pagina di destinazione. Passate un valore non negativo e il visualizzatore scorre in modo che quella posizione stia in cima alla finestra: usate la coordinata Y del titolo a cui state collegando. Lo zoom resta sempre invariato. Come per AddURILink, Description deve essere non vuota sotto PDFUACompliance e diventa il testo alternativo del link

HotPDF: indice collegato costruito con AddGoToLink che mostra salti con TargetPageIndex a base zero dalla pagina dell indice alle pagine dei capitoli, dove ogni titolo atterra in cima alla finestra
I rettangoli si estendono oltre il testo così che risponda l'intera riga, e una Y di atterraggio fissa colloca ogni titolo di capitolo in cima alla finestra
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;                        // la pagina 0 diventa l'indice

    // Crea prima le pagine dei capitoli così le destinazioni esistono
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // pagine 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Torna alla pagina 0 e disegna le voci dell'indice con i loro link
    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),    // copre la voce con un margine
        I + 1,                           // base zero: i capitoli sono le pagine 1..3
        780,                             // atterra con il titolo in cima
        AnsiString('Go to ' + Chapters[I]));
      Y := Y - 25;
    end;

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

Ogni voce riceve un rettangolo più largo del testo così che l'intera riga risponda al puntatore, e ogni link atterra con il titolo del capitolo (disegnato a Y=780) in cima alla finestra. Se in seguito inserite una pagina prima dei capitoli, ogni TargetPageIndex slitta di uno; calcolate gli indici dal vostro ciclo di creazione delle pagine invece di scriverli fissi

Un esempio completo di generazione di documento

Lo schema qui sotto mostra uno scenario più realistico: generare un breve rapporto con una sezione di intestazione, testo del corpo e una riga di link a piè di pagina, tutto da codice anziché da una maschera con campi 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;

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

    // Segnaposto del paragrafo di corpo
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Link a piè di pagina
    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;

Notate che SetFont viene chiamato prima di ogni gruppo di chiamate di testo. Il font non sopravvive ad AddPage, e se dimenticate di impostarlo prima di PrintHyperlink su una pagina nuova, il rettangolo dell'annotazione verrà calcolato con le metriche predefinite di quella pagina, che possono differire da ciò che vi aspettate

Dove la gestione delle annotazioni varia fra i visualizzatori

Le annotazioni URI del PDF sono definite in ISO 32000-1 §12.6.4.7, e ogni visualizzatore conforme dovrebbe seguirle. In pratica alcuni comportamenti differiscono da visualizzatore a visualizzatore. Adobe Acrobat mostra al primo clic una richiesta di sicurezza per gli URL non presenti nella lista dei domini attendibili; molti browser e lettori leggeri no. Alcuni visualizzatori PDF aziendali in ambienti bloccati disabilitano del tutto le annotazioni URI per politica, quindi il clic non fa nulla, senza errore visibile. Le app PDF per dispositivi mobili variano nell'aprire i link dentro la vista web dell'app oppure nel passarli al browser di sistema

Nessuno di questi è un difetto che possiate correggere dal lato della generazione; sono decisioni di politica del visualizzatore. Ciò che potete fare è scrivere etichette di link che rendano visibile l'URL anche nel corpo del documento, così un lettore in un ambiente limitato può comunque copiare l'indirizzo a mano. L'annotazione è la comodità; il testo è il ripiego

Un ulteriore dettaglio da conoscere: le annotazioni URI del PDF non portano alcuna sottolineatura visiva per impostazione predefinita. La sottolineatura che vedete nella maggior parte dei visualizzatori è disegnata dal visualizzatore stesso in base al tipo di annotazione, non da un glifo nel content stream. Se vi serve una sottolineatura fisica che sopravviva alla stampa verso un renderer non interattivo o alla conversione da PDF a immagine, disegnatela esplicitamente con LineTo e Stroke allo scostamento Y opportuno sotto la linea di base del testo. È una operazione di disegno separata, non qualcosa che PrintHyperlink gestisca al posto vostro

L'API dei collegamenti mostrata qui fa parte dello HotPDF Delphi Component per Delphi e C++Builder