Tekninen artikkeli

HotPDF Delphi Hyperlinkit: PrintHyperlink-annotaatiovinkit

PDF-hyperlinkit ovat URI-annotaatioita (URI annotations): suorakulmio (rectangle), joka peittää jonkin sivualueen ja joka napsautettuna (clicked) käskee katseluohjelman avaamaan URL-osoitteen. Annotaatio ja sen alla oleva teksti ovat täysin itsenäisiä objekteja. HotPDF:n PrintHyperlink niputtaa molemmat yhteen kutsuun, piirtäen tekstin ja laskien annotaation suorakulmion renderöidyn tekstin metriikasta (text metrics). Tämä mukavuus piilottaa yksityiskohdan, joka on syytä ymmärtää, ennen kuin kirjoitat tuotantokoodia. Se ei myöskään kerro koko totuutta: AddURILink sijoittaa napsautettavan alueen itse piirtämäsi sisällön päälle, ja AddGoToLink hoitaa sisäisen navigoinnin — molemmat käsitellään jäljempänä

Kuinka PrintHyperlink toimii

PrintHyperlink asuu THPDFPage-oliossa ja ottaa neljä argumenttia: X- ja Y-koordinaatit (pisteinä (points), origo vasemmassa alakulmassa, Y kasvaa ylöspäin), piirrettävän otsikkomerkkijonon (label string) ja URL-kohteen (target). Sisäisesti se kutsuu TextOut-funktiota nykyisellä hyperlinkkivärillä, ja laskee heti sen jälkeen annotaatiosuorakulmion käyttäen TextWidth- ja TextHeight-funktioita sen hetkisen fontin metriikoilla (font metrics). Se tarkoittaa, että fontti ja koko (size) on asetettava ennen kutsua, eivätkä ne saa muuttua otsikon piirtämisen ja annotaation asettamisen välillä, koska molemmat ratkaistaan samassa kutsussa

Oletusväri on clBlue. SetRGBHyperlinkColor muuttaa sen vain seuraavia kutsuja varten; se ei päivitä takautuvasti jo kirjoitettuja annotaatioita. Jos tarvitset eri värejä eri linkkiryhmille samalla sivulla, kutsu SetRGBHyperlinkColor ennen jokaista ryhmää ja palauta se sen jälkeen

Tässä on minimaalinen asiakirja, joka kirjoittaa kolme linkkiä kahdella eri värillä:

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

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

    // Oletussininen infolinkeille
    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');

    // Punainen toimintolinkille
    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);  // palauta oletus

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

Koordinaattiansa

HotPDF käyttää vasemmassa alakulmassa olevaa origoa, jossa Y kasvaa ylöspäin, mitattuna pisteinä (points, 1/72 tuumaa). A4-sivu on 595 x 842 pt; US Letter -sivu on 612 x 792 pt. Y=750 istuu lähellä A4-sivun yläreunaa, ja Y=50 olisi lähellä alareunaa. Kuka tahansa ruutugrafiikan (screen graphics) tai HTML:n parista tuleva olettaa päinvastaista ja sijoittaa ensimmäisen linkkirivin suoraan pois näkyvältä alueelta

Annotaatiosuorakulmio (annotation rectangle), jonka PrintHyperlink laskee, käyttää samaa koordinaatistoa. Jos myöhemmin kierrät (rotate) sivua, skaalaat (scale) sitä tai muutat sivukokoa (page size) laskematta X/Y-arvojasi uudelleen, näkyvä teksti ja napsautettava suorakulmio (clickable rectangle) ajelehtivat (drift) erilleen. Linkki "toimii" siinä mielessä, että jonnekin tekstin lähelle napsauttaminen laukaisee URL:n, mutta aktiivinen alue (hot zone) ei enää vastaa sitä mitä lukija näkee. Testaa sillä todellisella sivukoolla ja zoomaustasolla, jonka toimitat, ei pelkästään kehityskoneella 100 % tasolla

Yksi tapaus, jossa ajelehtiminen (drift) on taattu: jos kutsut PrintHyperlink-funktiota koordinaateilla, jotka sopivat A4-sivulle, ja vaihdat sitten mukautettuun (custom) kapeaformaattiseen sivuun (narrow-format page) säätämättä X/Y-arvoja, annotaatio voi päätyä kokonaan sivun ulkopuolelle. Annotaatio-objekti (annotation object) on silti kirjoitettu PDF-tiedostoon; useimmat katseluohjelmat leikkaavat (clip) sen äänettömästi pois, joten linkki yksinkertaisesti katoaa ilman mitään virhettä

Otsikkoteksti (Label text) vs. URL-kohde (target)

Text- ja Link-argumentit ovat riippumattomia. Voit piirtää "Download invoice PDF" samalla kun kohde (target) on täydellinen (fully qualified) HTTPS URL kyselyparametreineen (query parameters). Tuo erotus on tarkoituksellinen; näkyvän otsikon tulisi olla ihmisen luettavissa ja URL voi olla pitkä tai dynaamisesti luotu

Ongelmia syntyy silloin, kun otsikkona on pelkkä URL itse, varsinkin pitkä sellainen. Jos URL rivittyy (wraps) visuaalisesti kahdelle riville, mutta annotaatiosuorakulmio laskettiin yksiriviselle merkkijonolle, vain ensimmäinen rivi on napsautettavissa. PrintHyperlink ei käsittele monirivistä kulkua (multi-line flow); pidä otsikko (label) tarpeeksi lyhyenä mahtuakseen yhdelle riville nykyisellä fonttikoolla ja sivun leveydellä, käytä lyhyttä kuvailevaa otsikkoa täyden URL:n ollessa kohteena (target), tai sovella seuraavassa osiossa esitettyä rivikohtaista (per-line) kiertotapaa

Asiakirjoille, jotka arkistoidaan tai jaellaan ilman aktiivista verkkoyhteyttä, on myös syytä harkita, pitäisikö itse URL-osoitteen esiintyä painetussa muodossa jossain asiakirjan leipätekstissä, ei pelkästään annotaatiometadatana. Lukija, joka tulostaa PDF-tiedoston paperille, ei hyödy URI-annotaatiosta mitään

Monirivisen rajoituksen kiertäminen

Kun linkkiotsikon (link label) todella täytyy kattaa enemmän kuin yksi rivi — pitkä URL-osoite, joka tulostetaan sellaisenaan (verbatim), tai rivitetty lause, jonka pitäisi olla napsautettavissa päästä päähän — korjaus on lopettaa sen käsitteleminen yhtenä linkkinä ja käsitellä sitä yhtenä linkkinä per rivi. Jokainen PrintHyperlink-kutsu laskee suorakulmionsa tekstistä, jonka se piirtää, joten useat kutsut, jotka jakavat saman Link-kohteen (target), tuottavat useita oikean kokoisia annotaatioita, jotka kaikki avaavat saman URL-osoitteen. Lukija ei huomaa eroa; jokainen rivi reagoi napsautukseen

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;

// Käyttö: katkaise otsikko niistä kohdista, joissa asettelusi (layout) rivittää sen (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');

Merkkijonon pilkkominen (splitting) on sinun vastuullasi: katkaise se samoista kohdista, joissa se visuaalisesti rivittyisi (wrap) nykyisellä fontilla ja sarakkeen leveydellä, käyttäen TextWidth-funktiota jokaisen ehdokasrivin testaamiseen. Vaihtoehto on piirtää rivitetty teksti itse pelkillä TextOut-kutsuilla ja sitten asettaa yksi AddURILink-suorakulmio jokaisen rivin päälle — parempi reitti silloin, kun teksti on jo sinun oman sanarivityslogiikkasi (word-wrap logic) tuottama, mikä tuo meidät tuohon funktioon

AddURILink: napsautettavat alueet (clickable areas) minkä tahansa piirtämäsi päälle

PrintHyperlink on mukavuuskääre (convenience wrapper): se piirtää oman otsikkonsa (label) ja johtaa (derives) suorakulmion (rectangle) kyseisen otsikon metriikoista (metrics). AddURILink on suoraan paljastettu alemman tason puolikas:

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

Se kirjoittaa vain annotaation — tekstiä ei piirretä eikä väri muutu. Rectangle tulkitaan samassa koordinaattiavaruudessa kuin piirtokutsusikin (drawing calls), joten voit käyttää uudelleen (reuse) täsmälleen samoja X/Y-arvoja, jotka välitit TextOut- tai kuvakutsulle (image call). Se tekee siitä oikean työkalun aina, kun näkyvä sisältö on jo olemassa: kuvan hotspot (image hotspot), taulukon solu (table cell), aiemmin piirretty tekstilohko (block of text) tai yksi rivitetyn kappaleen rivi kuten yllä olevassa kiertotavassa. Annotaatio kantaa nollalevyistä (zero-width) reunaa, joten mikään näkyvä ei muutu; napsautettava alue on tarkalleen määrittämäsi suorakulmio

Funktio palauttaa annotaatiosanakirjan (annotation dictionary) THPDFDictionaryObject-oliona. Useimmat kutsujat hylkäävät tuloksen, mutta sen pitäminen (keeping it) antaa sinun säätää annotaation merkintöjä (entries) ennen kuin asiakirja on kirjoitettu

Kaksi vaatimustenmukaisuuden yksityiskohtaa (compliance details) on sisäänrakennettu. PDF/A-tiloissa (PDF/A modes) annotaation tulostuslippu (print flag) asetetaan, kuten nuo standardit vaativat. PDFUACompliance-asetuksen alla Description-parametrin on oltava ei-tyhjä merkkijono — siitä tulee annotaation /Contents-merkintä, joka on se mitä avustava teknologia (assistive technology) ilmoittaa (announces) linkille — ja kutsu laukaisee poikkeuksen (raises an exception) sen sijaan, että se äänettömästi säteilisi (silently emitting) epäyhdenmukaisen (non-conforming) tiedoston. PrintHyperlink on tuota sääntöä vanhempi (predates) eikä liitä kuvausta (description), joten PDF/UA-tulostetta varten piirrä otsikko (label) TextOut-funktiolla ja sijoita annotaatio AddURILink-kutsulla plus merkityksellisellä kuvauksella (meaningful description)

Päätössääntö on yksinkertainen: käytä PrintHyperlink-funktiota, kun linkki on lyhyt pätkä tekstiä, jota et ole vielä piirtänyt; käytä AddURILink-funktiota, kun napsautettava alue on sinun itsesi piirtämän tai mittaaman sisällön määrittelemä

Sisäinen navigointi AddGoToLinkillä

Ulkoiset URL-osoitteet (external URLs) ovat vain puolet siitä, mitä linkkiannotaatiot tekevät. Toinen puoli on navigointi asiakirjan sisällä — sisällysluettelo (table of contents), joka hyppää lukuihin (chapters), ristiviittaukset osioiden välillä (cross-references between sections). HotPDF paljastaa (exposes) tämän AddGoToLink-kutsun kautta:

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

Kolme semantiikkaa (semantics) on syytä todeta tarkasti, koska mikään niistä ei ole arvattavissa signatuurista (signature). TargetPageIndex on nollapohjainen (zero-based): asiakirjan ensimmäinen sivu on sivu 0, mikä vastaa CurrentPageNumber-ominaisuutta. Kohdesivun (target page) on oltava jo olemassa, kun teet kutsun; jos indeksi on alueen ulkopuolella (out of range), proseduuri palautuu (returns) lisäämättä annotaatiota — ei poikkeusta, ei linkkiä, ei varoitusta. Sisällysluettelolle, joka osoittaa eteenpäin, luo kaikki sivut ensin, vaihda sitten takaisin ja lisää linkit

YPos valitsee pystysuoran sijainnin kohdesivulla (target page), samassa koordinaattiavaruudessa kuin piirtokutsusikin (drawing calls). Oletusarvo -1 (mikä tahansa negatiivinen arvo) kirjoittaa nolla-kohdekoordinaatin (null destination coordinate), joka käskee katseluohjelmaa (viewer) pitämään nykyisen pystysuoran sijaintinsa (vertical position), kun se laskeutuu kohdesivulle. Anna ei-negatiivinen (non-negative) arvo ja katseluohjelma vierittää (scrolls) niin, että tuo sijainti asettuu ikkunan yläreunaan — käytä siihen otsikon Y-koordinaattia, johon olet linkittämässä. Zoomaus jätetään aina ennalleen. Kuten AddURILink-kutsussa, Description-parametrin on oltava ei-tyhjä (non-empty) PDFUACompliance-asetuksen alla ja siitä tulee linkin vaihtoehtoinen teksti (alternate text)

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;                        // sivu 0 tulee TOC-sivuksi (sisällysluettelo)

    // Luo lukujen sivut (chapter pages) ensin, jotta linkkikohteet (link targets) ovat olemassa
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // sivut 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Vaihda takaisin sivulle 0 ja piirrä TOC-merkinnät (entries) linkkeineen
    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),    // peittää merkinnän (entry) täytteellä (padding)
        I + 1,                           // nollapohjainen: luvut ovat sivuja 1..3
        780,                             // laskeutuu otsikko (heading) ikkunan yläreunassa
        AnsiString('Go to ' + Chapters[I]));
      Y := Y - 25;
    end;

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

Jokainen merkintä (entry) saa tekstiä leveämmän suorakulmion (rectangle), jotta koko rivi reagoi osoittimeen (pointer), ja jokainen linkki laskeutuu luvun otsikon (chapter heading) (piirretty kohdassa Y=780) ollessa ikkunan (window) yläreunassa. Jos myöhemmin lisäät sivun (insert a page) ennen lukuja, jokainen TargetPageIndex siirtyy (shifts) yhdellä; laske indeksit luontisilmukastasi (page-creation loop) sen sijaan, että koodaisit ne kiinteästi (hard-coding)

Täydellinen asiakirjanluomisesimerkki

Alla oleva kaava (pattern) näyttää realistisemman skenaarion: luodaan lyhyt raportti, jossa on ylätunnisteosio (header section), leipäteksti (body text) ja alatunnisterivi (footer row) linkkejä, kaikki koodista käsin sen sijaan, että ne tulisivat lomakkeelta TEdit-kentillä:

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;

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

    // Leipätekstikappaleen paikkamerkki (Body paragraph placeholder)
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');

    // Alatunnisteen linkit (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;

Huomaa, että SetFont kutsutaan ennen jokaista tekstikutsujen ryhmää. Fontti ei säily (persist) AddPage-kutsun yli, ja jos unohdat asettaa sen ennen PrintHyperlink-kutsua uudella sivulla, annotaatiosuorakulmio lasketaan mitä tahansa sivun oletusmetriikkoja (default metrics) vasten, jotka saattavat poiketa siitä mitä odotat

Missä annotaatioiden käsittely vaihtelee katseluohjelmittain

PDF URI -annotaatiot on määritelty ISO 32000-1 §12.6.4.7:ssä, ja jokaisen yhdenmukaisen (conforming) katseluohjelman tulisi noudattaa niitä. Käytännössä muutama käyttäytyminen eroaa katseluohjelmasta riippuen. Adobe Acrobat näyttää turvallisuuskehotteen (security prompt) ensimmäisellä napsautuksella (on first click) URL-osoitteille, jotka eivät ole luotettujen verkkotunnusten luettelossa (trusted domains list); monet selaimet (browsers) ja kevyet lukijat (lightweight readers) eivät näin tee. Jotkut yritystason (enterprise) PDF-katseluohjelmat lukituissa (locked-down) ympäristöissä poistavat URI-annotaatiot kokonaan käytöstä käytännön (policy) vuoksi, joten napsautus ei tee mitään, ilman näkyvää virhettä. Mobiili-PDF-sovellukset (Mobile PDF apps) vaihtelevat siinä, avaavatko ne linkit sovelluksen sisäisessä verkkonäkymässä (app's web view) vai siirtävätkö (hand off) ne tehtävän järjestelmän selaimelle

Mikään näistä ei ole bugi, jonka voisit korjata generointipuolelta (generation side); ne ovat katseluohjelman (viewer) käytäntöpäätöksiä (policy decisions). Mitä voit tehdä, on kirjoittaa linkkiotsikot (link labels) sellaisiksi, että URL on näkyvissä myös asiakirjan leipätekstissä (body), jotta lukija rajoitetussa ympäristössä voi silti kopioida (copy) osoitteen manuaalisesti. Annotaatio (annotation) on mukavuutta (convenience); teksti (text) on varakeino (fallback)

Vielä yksi yksityiskohta on syytä tietää: PDF URI -annotaatiot eivät sisällä oletusarvoisesti (by default) mitään visuaalista alleviivausta (visual underline). Alleviivaus, jonka näet useimmissa katseluohjelmissa (viewers), on katseluohjelman itsensä piirtämä annotaatiotyypin (annotation type) perusteella, eikä sisällön virran (content stream) glyyfi. Jos tarvitset fyysisen alleviivauksen, joka selviää (survives) tulostuksesta (printing) ei-interaktiiviselle (non-interactive) renderöijälle tai PDF-kuvaksi muuntamisesta (PDF-to-image conversion), piirrä se eksplisiittisesti (explicitly) LineTo- ja Stroke-kutsuilla sopivalla Y-siirtymällä (Y offset) tekstin perusviivan (text baseline) alapuolella. Se on erillinen piirtotoiminto, ei jotain mitä PrintHyperlink hoitaa puolestasi

Tässä esitelty hyperlinkki-API on osa HotPDF-komponenttia Delphille ja C++Builderille