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