Техническая статья

Гиперссылки HotPDF в Delphi: советы по аннотациям PrintHyperlink

Гиперссылки PDF — это аннотации URI: прямоугольник, покрывающий некоторую область страницы, который при нажатии сообщает программе просмотра открыть URL-адрес. Аннотация и текст под ней — совершенно независимые объекты. Функция HotPDF PrintHyperlink объединяет их в один вызов, рисуя текст и вычисляя прямоугольник аннотации на основе метрик отрендеренного текста. За этим удобством скрывается деталь, которую стоит понять, прежде чем писать рабочий код. Но это еще не все: AddURILink помещает интерактивную область над контентом, который вы нарисовали сами, а AddGoToLink обрабатывает внутреннюю навигацию — оба этих случая описаны ниже

Как работает PrintHyperlink

PrintHyperlink живет в THPDFPage и принимает четыре аргумента: координаты X и Y (в пунктах, начало координат в левом нижнем углу, Y увеличивается вверх), строку метки для рисования и целевой URL-адрес. Внутри он вызывает TextOut текущим цветом гиперссылки, затем немедленно вычисляет прямоугольник аннотации из TextWidth и TextHeight при текущих метриках шрифта. Это означает, что шрифт и размер должны быть установлены перед вызовом, и они не должны меняться между рисованием метки и размещением аннотации, поскольку оба разрешаются в одном вызове

Цвет по умолчанию — clBlue. SetRGBHyperlinkColor изменяет его только для последующих вызовов; он не обновляет ретроспективно уже написанные аннотации. Если вам нужны разные цвета для разных групп ссылок на одной странице, вызывайте SetRGBHyperlinkColor перед каждой группой и сбрасывайте его после этого

Вот минимальный документ, который записывает три ссылки двумя разными цветами:

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;

Ловушка координат

HotPDF использует начало координат в левом нижнем углу, причем Y увеличивается вверх, в пунктах (1/72 дюйма). Страница формата A4 имеет размер 595 x 842 пункта; страница формата US Letter — 612 x 792 пункта. Y=750 находится вблизи верхнего края страницы A4, а Y=50 будет около нижнего поля. Любой, кто перешел от экранной графики или HTML, предполагает обратное и размещает первую строку ссылки прямо за пределами видимой области

Прямоугольник аннотации, который вычисляет PrintHyperlink, использует ту же систему координат. Если вы позже повернете страницу, масштабируете ее или измените размер страницы без пересчета значений X/Y, видимый текст и интерактивный прямоугольник разойдутся. Ссылка "работает" в том смысле, что щелчок где-то рядом с текстом вызывает URL-адрес, но горячая зона больше не соответствует тому, что видит читатель. Проводите тестирование на реальном размере страницы и уровне масштабирования, которые вы поставляете, а не только на машине разработки при 100%

Один случай, когда дрейф гарантирован: если вы вызовете PrintHyperlink с координатами, подходящими для страницы формата A4, а затем переключитесь на пользовательскую страницу узкого формата без корректировки значений X/Y, аннотация может оказаться полностью за пределами страницы. Объект аннотации по-прежнему записывается в PDF; большинство программ для просмотра обрезают его молча, поэтому ссылка просто исчезает без какой-либо ошибки

Текст метки в сравнении с целевым URL-адресом

Аргументы Text и Link независимы. Вы можете нарисовать "Загрузить счет в формате PDF", в то время как целью будет полностью квалифицированный URL-адрес HTTPS с параметрами запроса. Это разделение преднамеренно; видимая метка должна быть удобочитаемой, а URL-адрес может быть длинным или сгенерированным динамически

Проблемы возникают, когда метка представляет собой сам необработанный URL-адрес, особенно если он длинный. Если URL визуально переносится на две строки, но прямоугольник аннотации был вычислен для однострочной строки, интерактивной будет только первая строка. PrintHyperlink не обрабатывает многострочный поток; держите метку достаточно короткой, чтобы она поместилась на одной строке при текущем размере шрифта и ширине страницы, используйте короткую описательную метку с полным URL-адресом в качестве цели или примените обходной путь для каждой строки, показанный в следующем разделе

Для документов, которые будут архивироваться или распространяться без активного подключения к Интернету, также подумайте, должен ли сам URL-адрес отображаться в печатном виде где-нибудь в теле документа, а не только как метаданные аннотации. Читатель, распечатывающий PDF на бумаге, ничего не получает от аннотации URI

Обход ограничения на несколько строк

Когда метка ссылки действительно должна занимать более одной строки — дословно напечатанный длинный URL-адрес или предложение с переносом, которое должно быть интерактивным от начала до конца — исправление заключается в том, чтобы перестать рассматривать ее как одну ссылку и рассматривать ее как одну ссылку на строку. Каждый вызов PrintHyperlink вычисляет свой прямоугольник из рисуемого им текста, поэтому несколько вызовов, использующих одну и ту же цель Link, создают несколько аннотаций правильного размера, которые все открывают один и тот же URL-адрес. Читатель не может заметить разницы; каждая строка реагирует на щелчок

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');

Разделение строки — это ваша обязанность: разбивайте ее в тех же позициях, где она визуально переносилась бы при текущем шрифте и ширине столбца, используя TextWidth для проверки каждой строки-кандидата. Альтернативой является рисование текста с переносом самостоятельно с помощью простых вызовов TextOut, а затем наложение одного прямоугольника AddURILink на каждую строку — лучший путь, когда текст уже создается вашей собственной логикой переноса слов, что подводит нас к этой функции

AddURILink: интерактивные области над всем, что вы нарисовали

PrintHyperlink — это удобная обертка: он рисует свою собственную метку и получает прямоугольник из метрик этой метки. AddURILink — это половина более низкого уровня, представленная напрямую:

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

Он записывает только аннотацию — текст не рисуется, и цвет не меняется. Rectangle интерпретируется в том же координатном пространстве, что и ваши вызовы рисования, поэтому вы можете повторно использовать точные значения X/Y, которые вы передали в TextOut или в вызов изображения. Это делает его подходящим инструментом, когда видимый контент уже существует: горячая зона на изображении, ячейка таблицы, нарисованный ранее блок текста или одна строка абзаца с переносом, как в обходном пути выше. Аннотация имеет границу нулевой ширины, поэтому визуально ничего не меняется; интерактивная область — это именно тот прямоугольник, который вы указываете

Функция возвращает словарь аннотаций в виде THPDFDictionaryObject. Большинство вызывающих программ отбрасывают результат, но его сохранение позволяет вам настроить записи аннотации перед записью документа

Встроены две детали совместимости. В режимах PDF/A флаг печати аннотации установлен в соответствии с требованиями этих стандартов. При PDFUACompliance параметр Description должен быть непустой строкой — он становится записью аннотации /Contents, которую объявляют вспомогательные технологии для ссылки — и вызов вызывает исключение вместо того, чтобы молча выдать файл, не соответствующий стандарту. PrintHyperlink предшествует этому правилу и не прикрепляет описание, поэтому для вывода PDF/UA нарисуйте метку с помощью TextOut и поместите аннотацию с помощью AddURILink плюс осмысленное описание

Правило принятия решения простое: используйте PrintHyperlink, когда ссылка представляет собой короткий фрагмент текста, который вы еще не нарисовали; используйте AddURILink, когда интерактивная область определяется контентом, который вы рисуете или измеряете сами

Внутренняя навигация с помощью AddGoToLink

Внешние URL-адреса — это лишь половина того, что делают аннотации ссылок. Другая половина — это навигация внутри документа: оглавление, переходящее к главам, перекрестные ссылки между разделами. HotPDF раскрывает это через AddGoToLink:

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

Стоит точно указать три семантики, поскольку ни об одной из них нельзя догадаться по сигнатуре. TargetPageIndex отсчитывается от нуля: первая страница документа — это страница 0, соответствующая CurrentPageNumber. Целевая страница уже должна существовать, когда вы делаете вызов; если индекс выходит за пределы диапазона, процедура возвращается без добавления аннотации — нет исключения, нет ссылки, нет предупреждения. Для оглавления, указывающего вперед, сначала создайте все страницы, затем вернитесь назад и добавьте ссылки

YPos выбирает вертикальную позицию на целевой странице в том же пространстве координат, что и ваши вызовы рисования. Значение по умолчанию -1 (любое отрицательное значение) записывает нулевую координату назначения, предписывая программе просмотра сохранять текущую вертикальную позицию при приземлении на целевую страницу. Передайте неотрицательное значение, и программа просмотра прокрутится так, чтобы эта позиция оказалась в верхней части окна — используйте координату Y заголовка, на который вы ссылаетесь. Масштаб всегда остается неизменным. Как и в случае с AddURILink, Description должно быть непустым при PDFUACompliance и становится альтернативным текстом ссылки

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;

Каждая запись получает прямоугольник шире текста, поэтому вся строка реагирует на указатель, и каждая ссылка приземляется так, что заголовок главы (нарисованный при Y=780) оказывается в верхней части окна. Если вы позже вставите страницу перед главами, каждый TargetPageIndex сместится на единицу; вычисляйте индексы в цикле создания страницы, а не жестко кодируйте их

Полный пример создания документа

Нижеприведенный шаблон показывает более реалистичный сценарий: создание короткого отчета с разделом заголовка, основным текстом и строкой ссылок в нижнем колонтитуле, причем все это выполняется из кода, а не из формы с полями 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;

Обратите внимание, что SetFont вызывается перед каждой группой текстовых вызовов. Шрифт не сохраняется при выполнении команды AddPage, и если вы забудете установить его перед PrintHyperlink на новой странице, прямоугольник аннотации будет вычисляться на основе метрик страницы по умолчанию, которые могут отличаться от ожидаемых

Где обработка аннотаций различается в разных программах просмотра

Аннотации PDF URI определены в ISO 32000-1 §12.6.4.7, и каждая соответствующая программа просмотра должна им следовать. На практике некоторые модели поведения различаются в зависимости от программы просмотра. Adobe Acrobat показывает запрос безопасности при первом щелчке по URL-адресам, которых нет в списке доверенных доменов; многие браузеры и легковесные программы для чтения этого не делают. Некоторые корпоративные программы для просмотра PDF в заблокированных средах полностью отключают аннотации URI в соответствии с политики, поэтому щелчок ничего не дает, без видимой ошибки. Мобильные приложения PDF различаются тем, открывают ли они ссылки внутри веб-представления приложения или передают их в системный браузер

Ничто из этого не является ошибкой, которую вы можете исправить со стороны генерации; это решения политики программы просмотра. Что вы можете сделать, так это написать метки ссылок, которые делают URL-адрес видимым также и в теле документа, чтобы читатель в ограниченной среде мог скопировать адрес вручную. Аннотация — это удобство; текст — это резервный вариант

Еще одна деталь, которую стоит знать: аннотации PDF URI по умолчанию не имеют визуального подчеркивания. Подчеркивание, которое вы видите в большинстве программ просмотра, рисуется самой программой просмотра на основе типа аннотации, а не глифом в потоке контента. Если вам нужно физическое подчеркивание, которое переживет печать в неинтерактивном средстве рендеринга или преобразование PDF в изображение, нарисуйте его явно с помощью LineTo и Stroke с соответствующим смещением по Y ниже базовой линии текста. Это отдельная операция рисования, а не то, что PrintHyperlink обрабатывает за вас

API гиперссылок, показанный здесь, является частью компонента HotPDF для Delphi и C++Builder