Teknisk artikkel

HotPDF Delphi Hyperkoblinger: Tips for PrintHyperlink-annoteringer

PDF-hyperkoblinger er URI-annoteringer: et rektangel som dekker et sideområde som, når det klikkes på, ber visningsprogrammet om å åpne en URL. Annoteringen og teksten under den er helt uavhengige objekter. HotPDFs PrintHyperlink bunter begge inn i ett kall, og tegner teksten og beregner annoteringsrektangelet fra de gjengitte tekstmålene. Den bekvemmeligheten skjuler en detalj det er verdt å forstå før du skriver produksjonskode. Det er heller ikke hele historien: AddURILink plasserer et klikkbart område over innhold du har tegnet selv, og AddGoToLink håndterer intern navigering — begge dekkes nedenfor

Hvordan PrintHyperlink fungerer

PrintHyperlink bor på THPDFPage og tar fire argumenter: X- og Y-koordinater (i punkter, med opprinnelse nede til venstre, Y økende oppover), etikettstrengen som skal tegnes, og URL-målet. Internt kaller den TextOut i den gjeldende hyperkoblingsfargen, og beregner deretter umiddelbart annoteringsrektangelet fra TextWidth og TextHeight ved de nåværende fontmålene (metrics). Det betyr at font og størrelse må settes før kallet, og de må ikke endres mellom tegning av etiketten og plassering av annoteringen, siden begge blir løst i det samme kallet

Standardfargen er clBlue. SetRGBHyperlinkColor endrer den kun for påfølgende kall; den oppdaterer ikke annoteringer som allerede er skrevet, med tilbakevirkende kraft. Hvis du trenger forskjellige farger for forskjellige lenkegrupper på samme side, kaller du SetRGBHyperlinkColor før hver gruppe og tilbakestiller den etterpå

Her er et minimalt dokument som skriver tre lenker med to forskjellige farger:

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

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

    // Standard blå for informasjonslenker
    Pdf.CurrentPage.TextOut(50, 750, 0, 'Referanselenker:');
    Pdf.CurrentPage.PrintHyperlink(50, 720, 'Produktside', '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');

    // Rød for handlingslenken
    Pdf.CurrentPage.SetRGBHyperlinkColor(clRed);
    Pdf.CurrentPage.PrintHyperlink(50, 660, 'Kjøp lisens', 'https://www.loslab.com/en-us/buy-hotpdf-fastspring.html');
    Pdf.CurrentPage.SetRGBHyperlinkColor(clBlue);  // gjenopprett standard

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

Koordinat-fellen

HotPDF bruker en opprinnelse (origin) nede til venstre med Y som vokser oppover, i punkter (1/72 tomme). En A4-side er 595 x 842 pt; en US Letter-side er 612 x 792 pt. Y=750 sitter nær toppen av en A4-side, og Y=50 vil være nær bunnmargen. Alle som kommer fra skjermgrafikk eller HTML, antar det motsatte og plasserer den første lenkelinjen rett utenfor det synlige området

Annoteringsrektangelet som PrintHyperlink beregner bruker samme koordinatsystem. Hvis du senere roterer siden, skalerer den, eller endrer sidestørrelsen uten å beregne dine X/Y-verdier på nytt, vil den synlige teksten og det klikkbare rektangelet drive fra hverandre. Lenken "fungerer" i den forstand at å klikke et sted nær teksten utløser URL-en, men den varme sonen (hot zone) samsvarer ikke lenger med hva leseren ser. Test på den faktiske sidestørrelsen og zoom-nivået du sender (ship), ikke bare på utviklingsmaskinen ved 100%

Ett tilfelle hvor driften er garantert: hvis du kaller PrintHyperlink med koordinater passende for en A4-side og deretter bytter til en egendefinert smal-format-side uten å justere X/Y-verdiene, kan annoteringen havne helt utenfor siden. Annoteringsobjektet skrives fremdeles inn i PDF-en; de fleste visningsprogrammer klipper (clip) den stille, så lenken forsvinner rett og slett uten noen feilmelding

Etiketteksten versus URL-mål

Argumentene Text og Link er uavhengige. Du kan tegne "Last ned faktura-PDF" mens målet er en fullt kvalifisert HTTPS-URL med spørringsparametere. Den separasjonen er bevisst; den synlige etiketten bør være lesbar for mennesker, og URL-en kan være lang eller genereres dynamisk

Det som skaper problemer er når etiketten er selve den rå URL-en, spesielt en lang en. Hvis URL-en brytes visuelt over to linjer, men annoteringsrektangelet ble beregnet for en enkelt-linje-streng, er bare den første linjen klikkbar. PrintHyperlink håndterer ikke flerlinjeflyt (multi-line flow); hold etiketten kort nok til at den passer på én linje ved gjeldende fontstørrelse og sidebredde, bruk en kort, beskrivende etikett med hele URL-en som mål, eller bruk per-linje-løsningen som er vist i neste seksjon

For dokumenter som vil bli arkivert eller distribuert uten en aktiv internettforbindelse, bør du også vurdere om selve URL-en skal vises i trykt form et sted i dokumentets brødtekst, ikke bare som annoteringsmetadata. En leser som skriver ut PDF-en på papir får ingenting ut av en URI-annotering

Omgå flerlinjebegrensningen

Når en lenke-etikett genuint må spenne over mer enn én linje — en lang URL skrevet ordrett (verbatim), eller en innpakket (wrapped) setning som skal være klikkbar fra ende til annen — er løsningen å slutte å behandle det som én lenke og behandle det som én lenke per linje. Hvert PrintHyperlink-kall beregner sitt rektangel fra teksten det tegner, slik at flere kall som deler samme Link-mål produserer flere riktig størrelsesannoteringer som alle åpner den samme URL-en. Leseren kan ikke se forskjellen; hver linje reagerer på et klikk

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;

// Bruk: bryt etiketten der din layout bryter den
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');

Å dele opp strengen er ditt ansvar: bryt den ved de samme posisjonene der den ville brytes visuelt ved gjeldende font- og kolonnebredde, ved å bruke TextWidth til å teste hver kandidatlinje. Alternativet er å tegne den brutte (wrapped) teksten selv med rene TextOut-kall og deretter legge ett AddURILink-rektangel over hver linje — en bedre rute når teksten allerede er produsert av din egen ordbrytingslogikk (word-wrap logic), som bringer oss til den funksjonen

AddURILink: klikkbare områder over alt du har tegnet

PrintHyperlink er en bekvemmelighetsinnpakning (convenience wrapper): den tegner sin egen etikett og utleder rektangelet fra denne etikettens målinger. AddURILink er den lavere nivå halvdelen som eksponeres direkte:

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

Den skriver kun annoteringen — ingen tekst tegnes og ingen farger endres. Rectangle tolkes i samme koordinatrom som tegnekallene dine, så du kan gjenbruke de nøyaktige X/Y-verdiene du sendte til TextOut eller et bildekall. Det gjør det til det riktige verktøyet når det synlige innholdet allerede eksisterer: en hotspot for bilder, en tabellcelle, en tekstblokk tegnet tidligere, eller én linje i et avsnitt med orddeling som i løsningen ovenfor. Annoteringen har en kantbredde (border width) på null, så ingenting synlig endres; det klikkbare området er nøyaktig det rektangelet du spesifiserer

Funksjonen returnerer annoteringsordboken som et THPDFDictionaryObject. De fleste som kaller funksjonen kaster (discard) resultatet, men å beholde det lar deg justere annoteringens oppføringer før dokumentet skrives

To samsvarsdetaljer (compliance details) er bygd inn. I PDF/A-moduser settes annoteringens utskriftsflagg (print flag) ettersom disse standardene krever det. Under PDFUAComplianceDescription-parameteren være en ikke-tom streng — den blir annoteringens /Contents-oppføring, som er hva assisterende teknologi annonserer for lenken — og kallet utløser et unntak (exception) snarere enn å i stillhet avgi en fil som ikke er i samsvar. PrintHyperlink ble til før den regelen og legger ikke ved noen beskrivelse, så for PDF/UA-utdata bør du tegne etiketten med TextOut og plassere annoteringen med AddURILink pluss en meningsfull beskrivelse

Beslutningsregelen er enkel: bruk PrintHyperlink når lenken er en kort bit med tekst du ikke har tegnet ennå; bruk AddURILink når den klikkbare regionen er definert av innhold du tegner eller måler selv

Intern navigering med AddGoToLink

Eksterne nettadresser (URL-er) er bare halvparten av det lenke-annoteringer gjør. Den andre halvparten er navigering inne i dokumentet — en innholdsfortegnelse som hopper til kapitler, kryssreferanser mellom seksjoner. HotPDF eksponerer dette gjennom AddGoToLink:

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

Tre semantikker er verdt å slå fast presist, siden ingen av dem er mulig å gjette fra signaturen. TargetPageIndex er nullbasert: den første siden i dokumentet er side 0, som samsvarer med CurrentPageNumber. Målsiden må allerede eksistere når du gjør kallet; hvis indeksen er utenfor rekkevidde (out of range), returnerer prosedyren uten å legge til en annotering — intet unntak (no exception), ingen lenke, ingen advarsel. For en innholdsfortegnelse som peker fremover, må du lage alle sidene først, for deretter å bytte tilbake og legge til lenkene

YPos velger den vertikale posisjonen på målsiden, i det samme koordinatrommet som tegnekallene dine. Standarden på -1 (enhver negativ verdi) skriver et nullmål-koordinat, som forteller fremviseren om å beholde sin nåværende vertikale posisjon når den lander på målsiden. Passerer du en ikke-negativ verdi, og fremviseren scroller slik at den posisjonen sitter øverst i vinduet — bruk Y-koordinaten til overskriften du lenker til. Zoom blir alltid stående uendret. Som med AddURILink, må Description være ikke-tom under PDFUACompliance og blir lenkens alternative tekst (alternate text)

procedure BuildLinkedTOC(const FileName: string);
const
  Chapters: array[0..2] of string =
    ('Introduksjon', 'Installasjon', 'API-referanse');
var
  Pdf: THotPDF;
  I, Y: Integer;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := FileName;
    Pdf.BeginDoc;                        // side 0 blir TOC-siden (innholdsfortegnelse)

    // Lag kapittelsidene først slik at lenkemålene eksisterer
    for I := 0 to High(Chapters) do
    begin
      Pdf.AddPage;                       // sider 1..3
      Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
      Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
    end;

    // Bytt tilbake til side 0 og tegn TOC-oppføringene med deres lenker
    Pdf.CurrentPageNumber := 0;
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
    Pdf.CurrentPage.TextOut(50, 760, 0, 'Innhold');
    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),    // dekker oppføringen med litt polstring (padding)
        I + 1,                           // nullbasert: kapitlene er side 1..3
        780,                             // land med overskriften på toppen
        AnsiString('Gå til ' + Chapters[I]));
      Y := Y - 25;
    end;

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

Hver oppføring får et rektangel som er bredere enn teksten slik at hele raden reagerer på pekeren, og hver lenke lander med kapitteloverskriften (tegnet ved Y=780) øverst i vinduet. Hvis du senere setter inn en side før kapitlene, forskyves hver TargetPageIndex med én; beregn indekser fra side-oppretting-løkken din snarere enn å hardkode dem

Et komplett dokumentgenerering-eksempel

Mønsteret nedenfor viser et mer realistisk scenario: generering av en kort rapport med en topptekst-seksjon, brødtekst og en bunntekst-rad (footer row) med lenker, alt fra kode i stedet for fra et skjema med TEdit-felt:

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;

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

    // Plassholder for avsnitt
    Pdf.CurrentPage.SetFont('Arial', [], 11);
    Pdf.CurrentPage.TextOut(50, 710, 0, 'Se lenkene nedenfor for fullstendig dokumentasjon.');

    // Bunntekst (Footer) lenker
    Pdf.CurrentPage.SetFont('Arial', [], 10);
    Pdf.CurrentPage.TextOut(50, 80, 0, 'Lenker:');
    Pdf.CurrentPage.PrintHyperlink(50, 60, 'Produktside', ProductURL);
    Pdf.CurrentPage.PrintHyperlink(200, 60, 'Støtte (Support)', SupportURL);

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

Legg merke til at SetFont blir kalt før hver gruppe med tekstkall. Fonten vedvarer ikke på tvers av AddPage, og hvis du glemmer å stille den inn før PrintHyperlink på en ny side, vil annoteringsrektangelet bli beregnet mot hva enn sidens standardmålinger (metrics) er, som kan avvike fra hva du forventer

Hvor annoteringshåndtering varierer på tvers av visningsprogrammer

PDF URI-annoteringer er definert i ISO 32000-1 §12.6.4.7, og hver konforme visningsapplikasjon bør følge dem. I praksis er det noen atferder som varierer etter visningsprogram. Adobe Acrobat viser en sikkerhetsadvarsel (security prompt) ved første klikk for nettadresser som ikke er i listen over klarerte domener; mange nettlesere og lettvektslesere gjør ikke det. Noen enterprise PDF-visere i nedlåste miljøer deaktiverer URI-annoteringer fullstendig gjennom retningslinjer (policy), så et klikk gjør ingenting, uten synlige feil. Mobil PDF-apper varierer i om de åpner lenker inne i appens web-visning eller overleverer til systemets nettleser

Ingen av disse er feil du kan fikse fra generasjonssiden; det er policybeslutninger for visningsprogrammet. Det du kan gjøre, er å skrive lenke-etiketter som gjør nettadressen (URL) synlig i selve dokumentteksten, slik at en leser i et begrenset miljø fortsatt kan kopiere adressen manuelt. Annoteringen er bekvemmeligheten; teksten er reserveløsningen (fallback)

Én detalj til som er verdt å vite: PDF URI-annoteringer har som standard ingen visuell understreking. Understrekingen du ser i de fleste fremvisere tegnes av visningsprogrammet selv basert på annoteringstypen, ikke av en glyff i innholdsstrømmen (content stream). Hvis du trenger en fysisk understrekning som overlever utskrift til en ikke-interaktiv gjengiver (renderer) eller PDF-til-bilde-konvertering, tegner du den eksplisitt med LineTo og Stroke på riktig Y-forskyvning under tekstens grunnlinje. Det er en separat tegneoperasjon, ikke noe PrintHyperlink håndterer for deg

Hyperkoblings-API-et som vises her er en del av HotPDF-komponenten for Delphi og C++Builder