Odborný článok

Hypertextové odkazy HotPDF v Delphi: PrintHyperlink

Hypertextové odkazy v PDF sú URI anotácie: obdĺžnik pokrývajúci časť plochy strany, ktorý po kliknutí povie prehliadaču, aby otvoril URL. Anotácia a text pod ňou sú úplne nezávislé objekty. Metóda PrintHyperlink v HotPDF spája oboje do jediného volania, vykreslí text a obdĺžnik anotácie spočíta z metrík vykresleného textu. Toto pohodlie skrýva detail, ktorý sa oplatí pochopiť skôr, než napíšete produkčný kód. A nie je to ani celý príbeh: AddURILink umiestni klikateľnú plochu nad obsah, ktorý ste nakreslili sami, a AddGoToLink rieši vnútornú navigáciu — oboje rozoberáme nižšie

Ako PrintHyperlink funguje

PrintHyperlink sídli na triede THPDFPage a berie štyri argumenty: súradnice X a Y (v bodoch, s počiatkom vľavo dole a Y rastúcim nahor), reťazec popisu na vykreslenie a cieľovú URL. Vnútri zavolá TextOut v aktuálnej farbe hypertextových odkazov a hneď potom spočíta obdĺžnik anotácie z TextWidth a TextHeight pri aktuálnych metrikách písma. To znamená, že písmo a veľkosť musia byť nastavené pred volaním a nesmú sa medzi vykreslením popisu a umiestnením anotácie zmeniť, pretože oboje sa vyhodnocuje v tom istom volaní

Anatómia jedného volania PrintHyperlink v HotPDF, ktoré zapíše dva nezávislé objekty PDF: viditeľné glyfy popisu vykreslené cez TextOut a obdĺžnik URI anotácie odkazu spočítaný z TextWidth a TextHeight
Glyfy popisu a obdĺžnik URI sú samostatné objekty PDF, a preto sa musí písmo aj farba odkazu ustáliť ešte pred jediným volaním, ktoré zapíše oboje

Predvolenou farbou je clBlue. SetRGBHyperlinkColor ju zmení len pre nasledujúce volania; už zapísané anotácie spätne neupraví. Ak potrebujete rôzne farby pre rôzne skupiny odkazov na tej istej strane, zavolajte SetRGBHyperlinkColor pred každou skupinou a potom ju vráťte späť

Tu je minimálny dokument, ktorý zapíše tri odkazy v dvoch rôznych farbách:

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

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

    // Predvolená modrá pre informačné odkazy
    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');

    // Červená pre akčný odkaz
    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);  // obnovenie predvolenej

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

Pasca so súradnicami

HotPDF používa počiatok vľavo dole s Y rastúcim nahor, v bodoch (1/72 palca). Strana A4 má 595 x 842 pt; strana US Letter má 612 x 792 pt. Y=750 sedí blízko horného okraja strany A4 a Y=50 by bolo blízko spodného okraja. Ktokoľvek prichádzajúci od obrazovkovej grafiky alebo HTML predpokladá opak a prvý riadok odkazov umiestni rovno mimo viditeľnú plochu

Obdĺžnik anotácie, ktorý PrintHyperlink spočíta, používa tú istú súradnicovú sústavu. Ak stranu neskôr otočíte, zmeníte jej mierku alebo veľkosť bez prepočítania hodnôt X/Y, viditeľný text a klikateľný obdĺžnik sa od seba vzdialia. Odkaz „funguje“ v tom zmysle, že kliknutie niekde blízko textu spustí URL, ale aktívna zóna už nezodpovedá tomu, čo čitateľ vidí. Testujte na tej veľkosti strany a úrovni priblíženia, ktorú naozaj dodávate, nielen na vývojárskom stroji pri 100 %

Jeden prípad, kde je posun zaručený: ak zavoláte PrintHyperlink so súradnicami vhodnými pre stranu A4 a potom prepnete na vlastný úzky formát strany bez úpravy hodnôt X/Y, anotácia môže skončiť celkom mimo strany. Objekt anotácie sa do PDF aj tak zapíše; väčšina prehliadačov ho ticho oreže, takže odkaz jednoducho zmizne bez akejkoľvek chyby

Text popisu verzus cieľová URL

Argumenty Text a Link sú nezávislé. Môžete vykresliť „Download invoice PDF“, kým cieľom je plne kvalifikovaná HTTPS adresa s parametrami dotazu. Toto oddelenie je zámerné; viditeľný popis má byť čitateľný pre človeka a URL môže byť dlhá alebo generovaná dynamicky

Problémy vznikajú vtedy, keď je popisom samotná surová URL, najmä ak je dlhá. Ak sa URL vizuálne zalomí do dvoch riadkov, no obdĺžnik anotácie bol spočítaný pre jednoriadkový reťazec, klikateľný je len prvý riadok. PrintHyperlink viacriadkové pretekanie nerieši; udržte popis dosť krátky, aby sa pri aktuálnej veľkosti písma a šírke strany zmestil na jeden riadok, použite krátky opisný popis s plnou URL ako cieľom, alebo uplatnite riešenie po riadkoch uvedené v ďalšej časti

Pri dokumentoch, ktoré sa budú archivovať alebo šíriť bez aktívneho pripojenia na internet, zvážte aj to, či by sa samotná URL nemala niekde v tele dokumentu objaviť v tlačenej podobe, nielen ako metadáta anotácie. Čitateľ, ktorý si PDF vytlačí na papier, z URI anotácie nemá nič

Ako obísť obmedzenie na jeden riadok

Keď popis odkazu naozaj musí zaberať viac než jeden riadok — dlhá URL vytlačená doslova alebo zalomená veta, ktorá má byť klikateľná od začiatku do konca — riešením je prestať ho brať ako jeden odkaz a brať ho ako jeden odkaz na riadok. Každé volanie PrintHyperlink si obdĺžnik spočíta z textu, ktorý vykreslí, takže niekoľko volaní zdieľajúcich rovnaký cieľ Link vyprodukuje niekoľko správne veľkých anotácií, ktoré všetky otvoria tú istú URL. Čitateľ rozdiel nespozná; na kliknutie reaguje každý riadok

Porovnanie zalomeného popisu odkazu v HotPDF, ktorý dostane jednu anotáciu pokrývajúcu len prvý riadok, oproti jednému volaniu PrintHyperlink na vykreslený riadok so zdieľaným cieľom URL
Obdĺžnik spočítaný pre jeden riadok necháva každé zalomené pokračovanie bez odkazu, kým volania po riadkoch zdieľajú cieľ a udržia klikateľný celý blok
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;

// Použitie: rozdeľte popis na miestach, kde ho vaše rozloženie zalamuje
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');

Rozdelenie reťazca je vašou zodpovednosťou: zalomte ho na tých istých miestach, kde by sa vizuálne zalomil pri aktuálnom písme a šírke stĺpca, pričom každý kandidátsky riadok otestujte cez TextWidth. Alternatívou je vykresliť zalomený text sami obyčajnými volaniami TextOut a potom nad každý riadok položiť jeden obdĺžnik AddURILink — lepšia cesta vtedy, keď text už produkuje vaša vlastná logika zalamovania slov, čo nás privádza k tejto funkcii

AddURILink: klikateľné plochy nad čímkoľvek, čo ste nakreslili

PrintHyperlink je pohodlný obal: kreslí si vlastný popis a obdĺžnik odvodzuje z metrík tohto popisu. AddURILink je jeho nižšia polovica vystavená priamo:

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

Zapisuje iba anotáciu — nekreslí žiadny text a nemení žiadne farby. Rectangle sa interpretuje v tom istom súradnicovom priestore ako vaše kresliace volania, takže môžete znovu použiť presne tie hodnoty X/Y, ktoré ste odovzdali metóde TextOut alebo volaniu s obrázkom. To z nej robí ten správny nástroj vždy, keď viditeľný obsah už existuje: aktívna plocha nad obrázkom, bunka tabuľky, blok textu vykreslený skôr alebo jeden riadok zalomeného odseku ako v riešení vyššie. Anotácia nesie nulovú šírku orámovania, takže sa vizuálne nič nezmení; klikateľnou oblasťou je presne ten obdĺžnik, ktorý zadáte

Funkcia vracia slovník anotácie ako THPDFDictionaryObject. Väčšina volajúcich výsledok zahodí, no jeho ponechanie vám umožní upraviť položky anotácie ešte pred zápisom dokumentu

Dva detaily súvisiace so zhodou s normami sú zabudované. V režimoch PDF/A sa tlačový príznak anotácie nastaví tak, ako tieto normy vyžadujú. Pod PDFUACompliance musí byť parameter Description neprázdnym reťazcom — stane sa položkou /Contents anotácie, teda tým, čo asistenčná technológia pri odkaze oznámi — a volanie radšej vyhodí výnimku, než by ticho vydalo nevyhovujúci súbor. PrintHyperlink je staršia než toto pravidlo a žiadny popis nepripája, takže pre výstup v PDF/UA vykreslite popis cez TextOut a anotáciu umiestnite cez AddURILink spolu so zmysluplným popisom

Rozhodovacie pravidlo je jednoduché: použite PrintHyperlink, keď je odkazom krátky kus textu, ktorý ste ešte nevykreslili; použite AddURILink, keď je klikateľná oblasť daná obsahom, ktorý kreslíte alebo meriate sami

Vnútorná navigácia cez AddGoToLink

Externé URL sú len polovicou toho, čo anotácie odkazov robia. Druhou polovicou je navigácia vnútri dokumentu — obsah, ktorý skáče na kapitoly, krížové odkazy medzi časťami. HotPDF ju sprístupňuje cez AddGoToLink:

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

Tri sémantické veci stojí za to povedať presne, keďže ani jednu zo signatúry neuhádnete. TargetPageIndex je počítaný od nuly: prvá strana dokumentu je stranou 0, zhodne s CurrentPageNumber. Cieľová strana musí v čase volania už existovať; ak je index mimo rozsahu, procedúra sa vráti bez pridania anotácie — žiadna výnimka, žiadny odkaz, žiadne varovanie. Pri obsahu, ktorý ukazuje dopredu, najprv vytvorte všetky strany a až potom sa vráťte a pridajte odkazy

YPos volí zvislú pozíciu na cieľovej strane, v tom istom súradnicovom priestore ako vaše kresliace volania. Predvolená hodnota -1 (akákoľvek záporná hodnota) zapíše nulovú cieľovú súradnicu a povie prehliadaču, aby si po pristátí na cieľovej strane ponechal aktuálnu zvislú pozíciu. Odovzdajte nezápornú hodnotu a prehliadač odroluje tak, aby táto pozícia sedela na vrchu okna — použite súradnicu Y nadpisu, na ktorý odkazujete. Priblíženie zostáva vždy nezmenené. Rovnako ako pri AddURILink musí byť Description pod PDFUACompliance neprázdny a stáva sa alternatívnym textom odkazu

HotPDF: Prepojený obsah vytvorený cez AddGoToLink zobrazujúci skoky podľa TargetPageIndex počítaného od nuly zo strany s obsahom na strany kapitol, kde každý nadpis pristane na vrchu okna
Obdĺžniky presahujú za text, takže reagujú celé riadky, a pevné pristávacie Y umiestni nadpis každej kapitoly na vrch okna
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;                        // strana 0 sa stane stranou s obsahom

    // Najprv vytvorte strany kapitol, aby ciele odkazov existovali
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // strany 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Vráťte sa na stranu 0 a vykreslite položky obsahu aj s ich odkazmi
    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),    // pokrýva položku aj s odsadením
        I + 1,                           // od nuly: kapitoly sú strany 1..3
        780,                             // pristátie s nadpisom na vrchu
        AnsiString('Go to ' + Chapters[I]));
      Y := Y - 25;
    end;

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

Každá položka dostane obdĺžnik širší než text, takže na ukazovateľ reaguje celý riadok, a každý odkaz pristane tak, že nadpis kapitoly (vykreslený na Y=780) je na vrchu okna. Ak neskôr vložíte stranu pred kapitoly, každý TargetPageIndex sa posunie o jednotku; indexy počítajte zo svojho cyklu vytvárania strán namiesto ich natvrdo zapísaných hodnôt

Kompletný príklad generovania dokumentu

Vzor nižšie ukazuje realistickejší scenár: generovanie krátkeho reportu s hlavičkou, textom tela a pätou s riadkom odkazov, všetko z kódu, nie z formulára s poliami 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;

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

    // Zástupný odsek tela
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Odkazy v päte
    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;

Všimnite si, že SetFont sa volá pred každou skupinou textových volaní. Písmo nepretrváva cez AddPage, a ak ho na novej strane zabudnete nastaviť pred PrintHyperlink, obdĺžnik anotácie sa spočíta voči tomu, aké sú predvolené metriky danej strany, čo sa môže líšiť od vášho očakávania

Kde sa spracovanie anotácií medzi prehliadačmi líši

URI anotácie v PDF sú definované v norme ISO 32000-1 §12.6.4.7 a každý vyhovujúci prehliadač by sa nimi mal riadiť. V praxi sa pár správaní podľa prehliadača líši. Adobe Acrobat pri prvom kliknutí zobrazí bezpečnostnú výzvu pre URL, ktoré nie sú v zozname dôveryhodných domén; mnohé webové prehliadače a odľahčené čítačky nie. Niektoré firemné prehliadače PDF v uzamknutých prostrediach URI anotácie politikou úplne zakazujú, takže kliknutie neurobí nič a bez viditeľnej chyby. Mobilné aplikácie na PDF sa líšia v tom, či odkazy otvárajú vo webovom pohľade v rámci aplikácie, alebo ich odovzdávajú systémovému prehliadaču

Nič z toho nie sú chyby, ktoré by ste opravili na strane generovania; sú to rozhodnutia politiky prehliadača. Čo urobiť môžete, je písať popisy odkazov tak, aby bola URL viditeľná aj v tele dokumentu, aby si ju čitateľ v obmedzenom prostredí mohol stále skopírovať ručne. Anotácia je pohodlím; text je záložným riešením

Ešte jeden detail stojí za poznanie: URI anotácie v PDF v predvolenom stave nenesú žiadne vizuálne podčiarknutie. Podčiarknutie, ktoré vo väčšine prehliadačov vidíte, kreslí sám prehliadač na základe typu anotácie, nie glyf v obsahovom streame. Ak potrebujete fyzické podčiarknutie, ktoré prežije tlač neinteraktívnym vykresľovačom alebo prevod PDF na obrázok, nakreslite ho výslovne cez LineTo a Stroke vo vhodnom odstupe Y pod účiarou textu. Je to samostatná kresliaca operácia, nie niečo, čo za vás rieši PrintHyperlink

Zobrazené API pre hypertextové odkazy je súčasťou komponentu HotPDF Delphi Component pre Delphi a C++Builder