기술 문서

HotPDF Delphi 하이퍼링크: PrintHyperlink 주석 팁

PDF 하이퍼링크는 URI 주석(annotations)입니다. 페이지의 일부 영역을 덮고 있는 사각형으로, 클릭하면 뷰어에게 URL을 열도록 지시합니다. 주석과 그 아래의 텍스트는 완전히 독립적인 개체입니다. HotPDF의 PrintHyperlink는 이 두 가지를 한 번의 호출로 묶어 텍스트를 그리고 렌더링된 텍스트 메트릭스(metrics)에서 주석 사각형을 계산합니다. 이러한 편리함은 실제 제작(production) 코드를 작성하기 전에 이해해 두어야 할 세부 사항을 숨기고 있습니다. 이것이 전부는 아닙니다. AddURILink는 사용자가 직접 그린 콘텐츠 위에 클릭 가능한 영역을 배치하고 AddGoToLink는 내부 탐색을 처리하는데, 두 가지 모두 아래에서 다루겠습니다

PrintHyperlink 작동 방식

PrintHyperlinkTHPDFPage에 존재하며 X 및 Y 좌표(포인트 단위, 좌측 하단 시작점, 위로 증가하는 Y값), 그릴 레이블 문자열, URL 타겟이라는 4개의 인수를 취합니다. 내부적으로는 현재 하이퍼링크 색상으로 TextOut을 호출한 다음 현재 글꼴 메트릭스의 TextWidthTextHeight를 통해 주석 사각형을 즉시 계산합니다. 즉, 호출 전에 글꼴과 크기를 설정해야 하며 레이블을 그리는 것과 주석을 배치하는 것 사이에 설정이 변경되어서는 안 됩니다. 둘 다 동일한 호출에서 해결(resolve)되기 때문입니다

기본 색상은 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는 포인트 단위(1/72인치)를 사용하며 좌측 하단을 시작점으로 Y값이 위로 증가하는 방식을 사용합니다. A4 페이지는 595 x 842 포인트이고 US Letter 페이지는 612 x 792 포인트입니다. Y=750은 A4 페이지의 맨 위 근처에 있고 Y=50은 아래쪽 여백 근처에 있습니다. 화면 그래픽이나 HTML 환경에서 온 사용자는 정반대로 가정하고 첫 번째 링크 줄을 화면 밖 보이지 않는 영역에 배치하게 될 수 있습니다

PrintHyperlink가 계산하는 주석 사각형은 동일한 좌표계를 사용합니다. 나중에 X/Y값을 다시 계산하지 않고 페이지를 회전하거나 크기를 조정하거나 페이지 크기를 변경하면 눈에 보이는 텍스트와 클릭 가능한 사각형이 서로 떨어져 이동하게 됩니다. 텍스트 근처 어딘가를 클릭하면 URL이 트리거(trigger)된다는 점에서는 링크가 "작동"하지만, 핫 존(hot zone)이 독자가 보는 영역과 더 이상 일치하지 않게 됩니다. 개발 머신에서 100% 비율로만 테스트하지 말고 실제로 제공(ship)할 페이지 크기와 배율 수준에서 테스트하세요

이러한 현상이 확실히 보장되는 한 가지 경우가 있습니다. A4 페이지에 적합한 좌표로 PrintHyperlink를 호출한 다음 X/Y값을 조정하지 않고 폭이 좁은 사용자 정의 형식 페이지로 전환하면 주석이 페이지를 완전히 벗어날 수 있습니다. 주석 객체는 여전히 PDF에 기록되어 있지만 대부분의 뷰어는 조용히 이 부분을 잘라내므로 오류 없이 단순히 링크가 사라지게 됩니다

레이블 텍스트와 URL 타겟 비교

TextLink 인수는 독립적입니다. "인보이스 PDF 다운로드"라는 텍스트를 그리면서 타겟은 쿼리 매개변수가 있는 완전히 검증된(fully qualified) HTTPS URL로 설정할 수 있습니다. 이러한 분리는 의도된 것입니다. 눈에 띄는 레이블은 사람이 읽을 수 있어야 하며, URL은 길 수도 있고 동적으로 생성될 수도 있기 때문입니다

문제가 되는 경우는 레이블이 원시(raw) URL 그 자체일 때이며, 특히 그 길이가 긴 경우입니다. URL이 시각적으로 두 줄에 걸쳐 줄 바꿈되지만 주석 사각형은 한 줄 문자열로 계산된 경우, 첫 번째 줄만 클릭할 수 있습니다. PrintHyperlink는 다중 줄 플로우를 처리하지 못합니다. 현재 글꼴 크기와 페이지 너비의 한 줄에 맞도록 레이블을 짧게 유지하거나, 전체 URL을 타겟으로 하는 짧은 설명 레이블을 사용하거나, 다음 섹션에 설명된 줄별 해결 방법(workaround)을 적용하십시오

인터넷에 연결되지 않은 상태에서 보관되거나 배포될 문서의 경우, 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를 사용하여 각각의 후보(candidate) 라인을 테스트하여 현재 글꼴 및 단 너비에서 시각적으로 줄이 바뀔 것과 동일한 위치에서 문자열을 나누어야 합니다. 대안은 일반 TextOut 호출을 사용하여 줄 바꿈된 텍스트를 직접 그린 다음 각 줄 위에 AddURILink 사각형을 배치하는 것입니다. 이는 텍스트가 이미 고유의 자동 줄 바꿈 로직에 의해 생성된 경우 더 나은 방법이며, 다음으로 그 기능에 대해 알아보겠습니다

AddURILink: 그린 모든 콘텐츠 위의 클릭 가능한 영역

PrintHyperlink는 편리한 래퍼(wrapper)입니다. 자체 레이블을 그리고 해당 레이블의 메트릭스(metrics)에서 사각형 영역을 도출합니다. 직접 노출되는 하위 수준 부분은 AddURILink입니다

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

이 함수는 오직 주석만 기록합니다. 텍스트를 그리지 않으며 색상 변화도 없습니다. Rectangle은 그리기 호출과 동일한 좌표 공간에서 해석되므로 TextOut이나 이미지 호출에 전달했던 정확한 X/Y값을 재사용할 수 있습니다. 따라서 이미지 핫스팟, 표의 셀, 이전에 그려진 텍스트 단락, 또는 위의 해결 방법과 같은 줄 바꿈된 단락의 한 줄 등 가시적인 콘텐츠가 이미 존재할 때 아주 유용한(right) 도구가 됩니다. 주석에는 너비가 0인 테두리가 있으므로 시각적인 변화는 발생하지 않으며 클릭 가능한 영역은 정확히 지정된 사각형 영역이 됩니다

이 함수는 주석 사전(annotation dictionary)을 THPDFDictionaryObject로 반환합니다. 대부분의 경우 이 결과를 무시하지만 이 결과를 유지하면 문서가 작성되기 전에 주석의 항목(entries)을 조정할 수 있습니다

여기에는 두 가지 규정 준수 관련 세부 사항이 내장되어 있습니다. PDF/A 모드에서는 해당 표준이 요구하는 대로 주석의 인쇄 플래그가 설정됩니다. PDFUACompliance 하에서는 Description 매개변수가 반드시 비어 있지 않은 문자열이어야 합니다(이는 주석의 /Contents 항목이 되며 지원 기술(assistive technology)이 해당 링크에 대해 안내하는 내용입니다). 그렇지 않으면 부적합한 파일을 조용히 내보내는 대신 호출 단계에서 예외(exception)를 발생시킵니다. PrintHyperlink는 이 규칙이 생기기 이전에 만들어져 설명을 첨부하지 않습니다. 따라서 PDF/UA 출력의 경우 TextOut을 사용하여 레이블을 그리고 AddURILink에 의미 있는 설명을 추가하여 주석을 배치하십시오

결정 규칙은 간단합니다. 아직 그리지 않은 짧은 텍스트에 링크를 적용할 때는 PrintHyperlink를 사용하고, 직접 그리거나 측정한 콘텐츠로 클릭 가능한 영역을 정의할 때는 AddURILink를 사용하세요

AddGoToLink를 사용한 내부 탐색

외부 URL은 링크 주석이 수행하는 역할의 절반에 불과합니다. 나머지 절반은 각 챕터로 건너뛰는 목차(table of contents), 각 섹션 간의 상호 참조 등 문서 내부 탐색입니다. HotPDF는 AddGoToLink를 통해 이를 제공(exposes)합니다

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

시그니처(signature)만으로는 추측하기 어려운 3가지 시맨틱스(semantics)에 대해 정확히 명시해둘 가치가 있습니다. TargetPageIndex는 0 기반(zero-based)입니다. 즉 CurrentPageNumber와 동일하게 문서의 첫 번째 페이지는 0입니다. 호출을 할 때 타겟 페이지가 이미 존재해야 합니다. 인덱스가 범위를 벗어나면 예외나 링크, 경고를 남기지 않고 주석을 추가하지 않은 채 프로시저가 반환됩니다. 앞을 가리키는(points forward) 목차의 경우 모든 페이지를 먼저 만든 다음 나중에 다시 돌아와서 링크를 추가하십시오

YPos는 그리기 호출 시 사용했던 것과 동일한 좌표 공간에서 타겟 페이지 상의 세로 위치를 선택합니다. 기본값인 -1(음수)은 널(null) 대상 좌표를 쓰게 되며 뷰어가 타겟 페이지에 도착했을 때 현재의 수직 위치를 유지하도록 지시합니다. 음수가 아닌 값을 전달하면 뷰어가 스크롤되어 해당 위치가 창의 상단에 놓이게 되므로, 링크하려는 헤딩(heading)의 Y 좌표를 사용하십시오. 배율(Zoom)은 항상 변경되지 않고 유지됩니다. AddURILink와 마찬가지로 PDFUACompliance 하에서 Description은 비어 있지 않아야 하며 해당 링크의 대체 텍스트가 됩니다

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;

각 항목(entry)은 텍스트보다 더 넓은 영역의 사각형을 할당받아 행 전체가 포인터에 반응하게 되고, 모든 링크는 도착 시 각 챕터의 헤딩(Y=780에 위치함)을 창 상단에 보여줍니다. 만약 나중에 챕터 앞에 페이지를 하나 추가한다면, 모든 TargetPageIndex는 1씩 밀려나게 됩니다. 따라서 인덱스를 하드코딩하기보다 페이지 생성 루프를 통해 계산하는 것이 좋습니다

완전한 문서 생성 예제

아래 패턴은 TEdit 필드가 있는 양식(form)이 아니라 코드를 통해서만 헤더(header) 섹션, 본문(body text) 및 링크로 구성된 바닥글(footer)을 포함한 짧은 보고서를 생성하는 좀 더 사실적인 시나리오를 보여줍니다

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 전에 글꼴 설정하는 것을 잊으면 페이지의 기본 메트릭스(metrics)에 따라 주석 사각형이 계산되어 여러분이 예상하는 것과 다른 결과가 나올 수 있습니다

뷰어에 따라 주석 처리가 달라지는 경우

PDF URI 주석은 ISO 32000-1 §12.6.4.7에 정의되어 있으며 호환되는 모든 뷰어는 이를 따라야 합니다. 그러나 실제로는 뷰어에 따라 몇 가지 동작이 달라집니다. Adobe Acrobat은 신뢰할 수 있는 도메인 목록에 없는 URL을 처음 클릭할 때 보안 프롬프트를 표시하지만 많은 브라우저와 가벼운 리더들은 그렇지 않습니다. 제한된 환경의 일부 엔터프라이즈 PDF 뷰어는 정책에 따라 URI 주석을 완전히 비활성화하므로 눈에 띄는 오류 없이 클릭해도 아무런 반응이 없습니다. 모바일 PDF 앱들은 앱 내부 웹 뷰(web view)에서 링크를 여는지 아니면 시스템 브라우저로 넘기는지에 따라 차이가 있습니다

이러한 문제는 문서 생성 측면에서 해결할 수 있는 버그가 아니며 뷰어 측의 정책적인 결정입니다. 여러분이 할 수 있는 것은 문서 본문에서도 URL을 볼 수 있도록 링크 레이블을 작성하여 제한된 환경의 독자가 주소를 수동으로 복사할 수 있도록 하는 것입니다. 주석은 편리함을 위한 것이고, 텍스트 자체는 안전망(fallback)의 역할을 합니다

알아두면 좋을 또 하나의 세부 사항이 있습니다. PDF URI 주석은 기본적으로 어떠한 시각적인 밑줄도 제공하지 않습니다. 대부분의 뷰어에서 볼 수 있는 밑줄은 콘텐츠 스트림 내의 글리프(glyph)가 아니라 주석 유형(type)을 바탕으로 뷰어 자체에서 그린 것입니다. 비대화형 렌더러로 인쇄하거나 PDF를 이미지로 변환할 때에도 유지되는 물리적인 밑줄이 필요하다면 텍스트 기준선(baseline) 아래 적절한 Y 오프셋에 위치하도록 LineToStroke를 사용하여 명시적으로 그려주어야 합니다. 이는 별개의 그리기 작업이며 PrintHyperlink가 대신 처리해주지 않습니다

여기에 나와 있는 하이퍼링크 API는 Delphi 및 C++Builder용 HotPDF 컴포넌트의 일부입니다