Hiperłącza PDF to adnotacje URI: prostokąt pokrywający pewien obszar strony, który po kliknięciu każe przeglądarce otworzyć adres URL. Adnotacja i tekst pod nią to zupełnie niezależne obiekty. Metoda PrintHyperlink w HotPDF łączy oba w jedno wywołanie, rysując tekst i obliczając prostokąt adnotacji na podstawie metryk wyrenderowanego tekstu. Ta wygoda ukrywa szczegół warty zrozumienia, zanim napiszesz kod produkcyjny. To też nie cała historia: AddURILink umieszcza klikalny obszar nad treścią, którą narysowałeś samodzielnie, a AddGoToLink obsługuje nawigację wewnątrz dokumentu — oba omówione poniżej
Jak działa PrintHyperlink
PrintHyperlink żyje w THPDFPage i przyjmuje cztery argumenty: współrzędne X i Y (w punktach, początek w lewym dolnym rogu, Y rosnące w górę), ciąg etykiety do narysowania oraz cel URL. Wewnętrznie wywołuje TextOut w bieżącym kolorze hiperłącza, a następnie natychmiast oblicza prostokąt adnotacji na podstawie TextWidth i TextHeight przy bieżących metrykach czcionki. Oznacza to, że czcionka i rozmiar muszą być ustawione przed wywołaniem i nie mogą zmienić się między narysowaniem etykiety a umieszczeniem adnotacji, ponieważ oba są rozstrzygane w tym samym wywołaniu
Domyślnym kolorem jest clBlue. SetRGBHyperlinkColor zmienia go tylko dla kolejnych wywołań; nie aktualizuje z mocą wsteczną adnotacji już zapisanych. Jeśli potrzebujesz różnych kolorów dla różnych grup łączy na tej samej stronie, wywołaj SetRGBHyperlinkColor przed każdą grupą i przywróć go później
Oto minimalny dokument, który zapisuje trzy łącza w dwóch różnych kolorach:
procedure CreateLinkedReport(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
Pdf.FileName := FileName;
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
// Default blue for informational links
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');
// Red for the action link
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); // restore default
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Pułapka układu współrzędnych
HotPDF używa początku w lewym dolnym rogu z Y rosnącym w górę, w punktach (1/72 cala). Strona A4 ma 595 x 842 pt; strona US Letter ma 612 x 792 pt. Y=750 znajduje się blisko górnej części strony A4, a Y=50 byłoby blisko dolnego marginesu. Każdy, kto przychodzi z grafiki ekranowej lub HTML, zakłada odwrotność i umieszcza pierwszy wiersz łącza wprost poza widocznym obszarem
Prostokąt adnotacji, który oblicza PrintHyperlink, używa tego samego układu współrzędnych. Jeśli później obrócisz stronę, przeskalujesz ją lub zmienisz rozmiar strony bez przeliczenia wartości X/Y, widoczny tekst i klikalny prostokąt oddalą się od siebie. Łącze „działa” w tym sensie, że kliknięcie gdzieś w pobliżu tekstu wyzwala URL, ale strefa aktywna nie odpowiada już temu, co widzi czytelnik. Testuj na rzeczywistym rozmiarze strony i poziomie powiększenia, jaki dostarczasz, a nie tylko na maszynie deweloperskiej przy 100%
Jeden przypadek, w którym rozjazd jest gwarantowany: jeśli wywołasz PrintHyperlink ze współrzędnymi odpowiednimi dla strony A4, a następnie przełączysz się na niestandardową stronę w wąskim formacie bez dostosowania wartości X/Y, adnotacja może w całości wylądować poza stroną. Obiekt adnotacji jest nadal zapisywany w PDF; większość przeglądarek po cichu go przycina, więc łącze po prostu znika bez żadnego błędu
Tekst etykiety a cel URL
Argumenty Text i Link są niezależne. Możesz narysować „Download invoice PDF”, podczas gdy celem jest w pełni kwalifikowany adres HTTPS z parametrami zapytania. To rozdzielenie jest celowe; widoczna etykieta powinna być czytelna dla człowieka, a URL może być długi lub generowany dynamicznie
Problemy tworzą się, gdy etykietą jest surowy sam URL, zwłaszcza długi. Jeśli URL wizualnie zawija się na dwa wiersze, ale prostokąt adnotacji został obliczony dla ciągu jednowierszowego, klikalny jest tylko pierwszy wiersz. PrintHyperlink nie obsługuje przepływu wielowierszowego; utrzymuj etykietę na tyle krótką, by zmieściła się w jednym wierszu przy bieżącym rozmiarze czcionki i szerokości strony, użyj krótkiej opisowej etykiety z pełnym URL jako celem lub zastosuj obejście na wiersz pokazane w następnej sekcji
W przypadku dokumentów, które będą archiwizowane lub dystrybuowane bez aktywnego połączenia internetowego, rozważ również, czy sam URL nie powinien pojawić się w formie drukowanej gdzieś w treści dokumentu, a nie tylko jako metadane adnotacji. Czytelnik drukujący PDF na papierze nie dostaje nic z adnotacji URI
Obejście ograniczenia wielowierszowego
Gdy etykieta łącza naprawdę musi obejmować więcej niż jeden wiersz — długi URL wydrukowany dosłownie lub zawinięte zdanie, które ma być klikalne od początku do końca — rozwiązaniem jest przestać traktować je jako jedno łącze i potraktować jako jedno łącze na wiersz. Każde wywołanie PrintHyperlink oblicza swój prostokąt na podstawie rysowanego tekstu, więc kilka wywołań dzielących ten sam cel Link tworzy kilka poprawnie zwymiarowanych adnotacji, które wszystkie otwierają ten sam URL. Czytelnik nie zauważa różnicy; każdy wiersz reaguje na kliknięcie
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;
// Usage: break the label at the positions where your layout 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');
Podział ciągu należy do ciebie: dziel go w tych samych miejscach, w których wizualnie zawinąłby się przy bieżącej czcionce i szerokości kolumny, używając TextWidth do przetestowania każdego kandydata na wiersz. Alternatywą jest samodzielne narysowanie zawiniętego tekstu zwykłymi wywołaniami TextOut, a następnie nałożenie jednego prostokąta AddURILink na każdy wiersz — lepsza droga, gdy tekst jest już produkowany przez twoją własną logikę zawijania słów, co prowadzi nas do tej funkcji
AddURILink: klikalne obszary nad wszystkim, co narysowałeś
PrintHyperlink jest wygodną nakładką: rysuje własną etykietę i wyprowadza prostokąt z metryk tej etykiety. AddURILink to niższopoziomowa połowa udostępniona wprost:
function AddURILink(Rectangle: TRect; const URL: AnsiString;
const Description: AnsiString = ''): THPDFDictionaryObject;
Zapisuje tylko adnotację — żaden tekst nie jest rysowany i żaden kolor nie zmienia się. Rectangle jest interpretowany w tej samej przestrzeni współrzędnych co twoje wywołania rysowania, więc możesz ponownie użyć dokładnie tych wartości X/Y, które przekazałeś do TextOut lub wywołania obrazu. To czyni ją właściwym narzędziem, gdy widoczna treść już istnieje: hotspot obrazu, komórka tabeli, blok tekstu narysowany wcześniej lub jeden wiersz zawiniętego akapitu jak w obejściu powyżej. Adnotacja niesie obramowanie o zerowej szerokości, więc nic widocznego się nie zmienia; klikalny obszar to dokładnie prostokąt, który podasz
Funkcja zwraca słownik adnotacji jako THPDFDictionaryObject. Większość wywołujących odrzuca wynik, ale jego zachowanie pozwala dostosować wpisy adnotacji, zanim dokument zostanie zapisany
Dwa szczegóły zgodności są wbudowane. W trybach PDF/A flaga drukowania adnotacji jest ustawiana zgodnie z wymaganiami tych standardów. Pod PDFUACompliance parametr Description musi być niepustym ciągiem — staje się wpisem /Contents adnotacji, czyli tym, co technologia wspomagająca ogłasza dla łącza — a wywołanie zgłasza wyjątek, zamiast po cichu emitować niezgodny plik. PrintHyperlink jest starszy niż ta reguła i nie dołącza żadnego opisu, więc dla wyjścia PDF/UA narysuj etykietę za pomocą TextOut i umieść adnotację za pomocą AddURILink z sensownym opisem
Reguła decyzyjna jest prosta: użyj PrintHyperlink, gdy łącze to krótki fragment tekstu, którego jeszcze nie narysowałeś; użyj AddURILink, gdy klikalny obszar jest zdefiniowany przez treść, którą sam rysujesz lub mierzysz
Nawigacja wewnątrz dokumentu z AddGoToLink
Zewnętrzne adresy URL to tylko połowa tego, co robią adnotacje łączy. Druga połowa to nawigacja wewnątrz dokumentu — spis treści przeskakujący do rozdziałów, odsyłacze między sekcjami. HotPDF udostępnia to przez AddGoToLink:
procedure AddGoToLink(Rectangle: TRect; TargetPageIndex: Integer;
YPos: Single = -1; const Description: AnsiString = '');
Trzy semantyki warto sformułować precyzyjnie, ponieważ żadnej nie da się odgadnąć z sygnatury. TargetPageIndex jest liczony od zera: pierwsza strona dokumentu to strona 0, zgodnie z CurrentPageNumber. Strona docelowa musi już istnieć w momencie wywołania; jeśli indeks jest poza zakresem, procedura kończy się bez dodania adnotacji — bez wyjątku, bez łącza, bez ostrzeżenia. Dla spisu treści wskazującego w przód utwórz najpierw wszystkie strony, a potem wróć i dodaj łącza
YPos wybiera pozycję pionową na stronie docelowej, w tej samej przestrzeni współrzędnych co twoje wywołania rysowania. Domyślne -1 (dowolna wartość ujemna) zapisuje pustą współrzędną celu, mówiąc przeglądarce, by zachowała bieżącą pozycję pionową po wylądowaniu na stronie docelowej. Przekaż wartość nieujemną, a przeglądarka przewinie się tak, że ta pozycja znajdzie się u góry okna — użyj współrzędnej Y nagłówka, do którego prowadzisz łącze. Powiększenie zawsze pozostaje niezmienione. Podobnie jak w AddURILink, Description musi być niepusty pod PDFUACompliance i staje się tekstem alternatywnym łącza
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; // page 0 becomes the TOC page
// Create the chapter pages first so the link targets exist
for I := 0 to High(Chapters) do
begin
Pdf.AddPage; // pages 1..3
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 780, 0, Chapters[I]);
end;
// Switch back to page 0 and draw the TOC entries with their links
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), // covers the entry with padding
I + 1, // zero-based: chapters are pages 1..3
780, // land with the heading at the top
AnsiString('Go to ' + Chapters[I]));
Y := Y - 25;
end;
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
Każdy wpis otrzymuje prostokąt szerszy niż tekst, aby cały wiersz reagował na wskaźnik, a każde łącze ląduje z nagłówkiem rozdziału (narysowanym przy Y=780) u góry okna. Jeśli później wstawisz stronę przed rozdziałami, każdy TargetPageIndex przesunie się o jeden; obliczaj indeksy na podstawie pętli tworzenia stron zamiast wpisywać je na sztywno
Kompletny przykład generowania dokumentu
Wzorzec poniżej pokazuje bardziej realistyczny scenariusz: generowanie krótkiego raportu z sekcją nagłówka, tekstem treści i wierszem stopki z łączami, wszystko z kodu, a nie z formularza z polami 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;
// Header
Pdf.CurrentPage.SetFont('Arial', [fsBold], 16);
Pdf.CurrentPage.TextOut(50, 750, 0, WideString(ProductName));
// Body paragraph placeholder
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 710, 0, 'See the links below for full documentation.');
// 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;
Zauważ, że SetFont jest wywoływany przed każdą grupą wywołań tekstu. Czcionka nie utrzymuje się przez AddPage, a jeśli zapomnisz ustawić ją przed PrintHyperlink na nowej stronie, prostokąt adnotacji zostanie obliczony względem domyślnych metryk strony, które mogą różnić się od tego, czego oczekujesz
Gdzie obsługa adnotacji różni się między przeglądarkami
Adnotacje URI PDF są zdefiniowane w ISO 32000-1 §12.6.4.7 i każda zgodna przeglądarka powinna ich przestrzegać. W praktyce kilka zachowań różni się między przeglądarkami. Adobe Acrobat pokazuje monit bezpieczeństwa przy pierwszym kliknięciu dla adresów URL spoza listy zaufanych domen; wiele przeglądarek i lekkich czytników tego nie robi. Niektóre firmowe przeglądarki PDF w zablokowanych środowiskach całkowicie wyłączają adnotacje URI zgodnie z polityką, więc kliknięcie nic nie robi, bez widocznego błędu. Mobilne aplikacje PDF różnią się tym, czy otwierają łącza wewnątrz widoku sieciowego aplikacji, czy przekazują je do przeglądarki systemowej
Żadne z tych nie są błędami, które możesz naprawić po stronie generowania; są to decyzje polityki przeglądarki. To, co możesz zrobić, to pisać etykiety łączy, które sprawiają, że URL jest widoczny również w treści dokumentu, tak by czytelnik w ograniczonym środowisku wciąż mógł ręcznie skopiować adres. Adnotacja to wygoda; tekst to zabezpieczenie awaryjne
Jeszcze jeden szczegół warty poznania: adnotacje URI PDF domyślnie nie niosą żadnego wizualnego podkreślenia. Podkreślenie, które widzisz w większości przeglądarek, rysuje sama przeglądarka na podstawie typu adnotacji, a nie glif w strumieniu treści. Jeśli potrzebujesz fizycznego podkreślenia, które przetrwa drukowanie do nieinteraktywnego renderera lub konwersję PDF-do-obrazu, narysuj je jawnie za pomocą LineTo i Stroke przy odpowiednim przesunięciu Y poniżej linii bazowej tekstu. To osobna operacja rysowania, a nie coś, co PrintHyperlink obsługuje za ciebie
Pokazane tutaj API hiperłączy jest częścią HotPDF Component dla Delphi i C++Builder