기술 문서

Delphi의 HotPDF TextOut: 크기, 스타일, 회전 및 간격

HotPDF 문서의 모든 시각적 문자열은 TextOut(X, Y, angle, Text) 호출을 통해 전달됩니다. Hello World 예제는 이를 가장 단순하게 사용하여 글꼴을 한 번 설정하고 4개의 인수를 적절한 기본값으로 둡니다. 하지만 첫 페이지를 넘어가면 이 4개의 인수가 레이아웃의 모든 역할을 담당하게 됩니다. 세 번째 인수는 텍스트 출력(run)을 회전시킵니다. 그 직전에 설정된 글꼴은 크기와 스타일을 결정합니다. 그리고 페이지 모서리부터 포인트(pt) 단위로 측정된 X, Y 좌표 쌍은 깔끔한 보고서와 겹치거나 잘리거나 다른 프린터에서 한 줄 아래로 밀리는 텍스트 사이를 구분하는 유일한 요소입니다. 바로 이 부분이 TextOut이 제 역할을 다하는 곳이며, 기본값만으로는 부족해지는 지점입니다

무엇보다 먼저 시그니처(signature)를 기억해 둘 필요가 있습니다. XY는 포인트 단위의 Single이고, angle은 각도(도) 단위의 Extended이며, TextWideString이므로 별도의 호출 없이 유니코드가 그대로 통과됩니다. 두 번째 오버로드(overload)는 이미 글리프(glyph) 코드를 보유하고 있을 때 사용할 수 있도록 PWORD와 길이를 취하지만, 일반적인 문자열의 경우에는 WideString 형식을 사용하게 됩니다

크기와 스타일은 TextOut이 아닌 SetFont에서 지정

TextOut에는 크기 매개변수가 없습니다. 크기, 굵기, 기울임 등 모든 것은 텍스트를 출력하기 전의 SetFont 호출에 정의되어 있으며, 이는 다음 SetFont가 이를 대체할 때까지 유효합니다. 이것이 사용 첫날의 대부분의 혼란을 설명하는 단일한 사실입니다. 어떤 줄이 굵게 출력되는 이유는, 3번의 호출 전에 무언가가 [fsBold]를 설정했고 그 설정이 해제되지 않았기 때문입니다

Pdf.CurrentPage.SetFont('Times New Roman', [], 24);
Pdf.CurrentPage.TextOut(72, 740, 0, 'Quarterly Report');        // 24pt regular

Pdf.CurrentPage.SetFont('Times New Roman', [fsBold], 12);
Pdf.CurrentPage.TextOut(72, 712, 0, 'Revenue');                 // 12pt bold

Pdf.CurrentPage.SetFont('Times New Roman', [fsItalic], 11);
Pdf.CurrentPage.TextOut(72, 694, 0, 'figures in thousands');    // 11pt italic

Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsItalic], 10);
Pdf.CurrentPage.TextOut(72, 676, 0, '  +18.4% YoY');            // styles combine

두 번째 인수는 TFontStyles 집합이므로 [fsBold, fsItalic]은 굵은 기울임꼴이고 []은 일반(plain) 텍스트를 의미합니다. 크기는 좌표와 동일한 단위인 포인트로 표시되어 수직 간격을 파악하기 쉽게 해줍니다. 12포인트 텍스트 줄은 여백을 위해 수직으로 대략 14~16포인트 정도의 간격이 필요하므로 줄당 Y값을 14씩 낮추는 것이 적절한 시작 줄 간격(leading)입니다. 자동 줄 바꿈은 없습니다. 사용자가 각 기준선(baseline)을 직접 계산해야 하는데, 이는 단락을 구성할 때는 번거롭지만 모든 필드가 고정된 좌표에 위치해야 하는 양식(form)에서는 정확성을 보장합니다

글꼴 이름과 관련된 두 가지 실용적인 참고 사항입니다. 글꼴은 빌드 컴퓨터에 설치된 글꼴을 기준으로 확인되며, OS가 반환하는 것이 그대로 임베디드(embedded)되므로 개발자의 데스크톱에서 확인되는 글꼴과 빌드 서버에서 확인되는 글꼴이 동일한 서체라는 보장이 없습니다. 또한 글꼴은 문자열의 모든 문자를 포함해야 합니다. 라틴어 전용 글꼴에서 키릴 문자나 CJK 텍스트를 출력하면 아무런 오류 없이 문자가 깨진 상자 모양(missing-glyph boxes)으로 렌더링되므로, 여러 언어가 혼합될 때는 Hello World 페이지에서 광범위한 유니코드 서체를 사용하는 것이 좋습니다

여러 문자 집합에서 일반, 굵게, 기울임꼴 스타일로 렌더링된 Arial, Times New Roman 및 Courier New를 보여주는 HotPDF TextOut 페이지

angle 인수는 기준점(anchor)을 중심으로 회전합니다

세 번째 인수는 대부분의 코드에서 항상 0으로 두는 매개변수입니다. 0이 아닌 값을 전달하면 텍스트 출력(run)이 텍스트의 좌측 하단인 (X, Y) 기준점을 중심으로 전달된 각도만큼 시계 반대 방향으로 회전합니다. 기준점 자체는 이동하지 않으므로, 수평 레이블을 배치한 것과 동일한 좌표가 회전된 레이블의 기준이 됩니다. 단지 글리프가 그려지는 방향만 바뀔 뿐입니다

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

// A vertical axis label down the left margin: 90 degrees reads bottom-to-top.
Pdf.CurrentPage.TextOut(40, 300, 90, 'Units sold');

// A diagonal DRAFT watermark across the page body.
Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');

// Column headers tilted 60 degrees so long labels fit a narrow table.
Pdf.CurrentPage.SetFont('Arial', [], 9);
Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');

차트 옆면을 따라 올라가는 레이블이나 책등 제목과 같은 경우 보통 90도를 사용합니다. 45도는 열 헤더를 기울일 때 사용하며, 이 방법은 옆 열을 침범하지 않고 좁은 열 위에 넓은 레이블을 배치하는 유용한 팁입니다. 회전은 기준점이 해석되는 방식을 변경하지 않는데, 이것이 종종 혼란을 야기합니다. 90도로 회전된 텍스트도 여전히 (X, Y)에서 시작하여 위로 자라나게 되므로 회전된 레이블을 중앙에 맞추려면 각도가 아닌 기준점을 조정해야 합니다. 여러 개의 회전된 텍스트가 기준선을 공유할 때는 수평 줄을 쌓을 때 Y값을 이동하는 것과 정확히 동일한 방식으로, 동일한 Y값을 주고 X값을 이동시키면 됩니다

추측 없는 좌표 배치

좌표는 코드 리뷰에서 통과하거나 조용히 실패를 일으키는 부분입니다. HotPDF는 페이지의 왼쪽 하단 모서리를 기준으로 측정하며, 인치당 72포인트 단위로 Y값이 위로 증가합니다. US Letter 페이지는 612 x 792 포인트이고, A4는 595 x 842 포인트입니다. 따라서 Letter 크기에서 상단 여백을 1인치로 두려면 첫 번째 기준선이 위쪽의 어떤 작은 숫자가 아니라 대략 Y = 792 - 72 - (글꼴 크기) 근처가 됩니다. Y값이 0부터 아래로 증가하는 화면 좌표계에 익숙한 사람이라면 첫 줄을 맨 아래 가장자리를 벗어난 곳에 쓰게 되어 10분 동안 그 내용이 어디로 갔는지 찾게 될 수 있습니다

레이블들의 블록을 구성할 때는 마법의 숫자(magic numbers)들로 채우는 대신, 지정된 기준점을 바탕으로 한 산술 연산으로 레이아웃을 다뤄야 합니다. 왼쪽 여백, 각 줄마다 감소하는 기준선, 그리고 고정된 줄 간격(leading)을 활용하면 하드코딩된 값들의 나열 대신 간결한 루프로 작성할 수 있습니다

const
  LeftMargin = 72;        // 1 inch in
  TopBaseline = 720;       // first line, ~1 inch down on Letter
  Leading = 16;            // vertical step between lines
var
  Y: Single;
  Line: string;
begin
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Y := TopBaseline;
  for Line in ReportLines do
  begin
    Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Line);
    Y := Y - Leading;
    if Y < 72 then            // bottom margin reached
    begin
      Pdf.AddPage;
      Pdf.CurrentPage.SetFont('Arial', [], 11);  // font resets on a new page
      Y := TopBaseline;
    end;
  end;
end;

페이지 구분 보호(page-break guard) 코드는 사람들이 가장 먼저 잊어버리는 것이며, 실제 환경에서 가장 큰 문제가 발생합니다. TextOut의 이면에는 플로우(flow) 레이아웃이라는 것이 없습니다. 하단 여백을 지나서 계속 감소하면 아무런 경고 없이 여백 바깥, 페이지 밖, 그리고 아무것도 없는 곳에 계속해서 텍스트를 그립니다. 따라서 사용자가 스스로 Y값을 주시하다가 바닥을 지날 때 AddPage를 호출하고 기준선을 재설정해야 합니다. AddPage 이후의 SetFont는 생략해도 되는 여분이 아닙니다. 현재 설정된 글꼴은 페이지가 바뀔 때 유지되지 않기 때문에 이 단계를 건너뛰면 새 페이지의 첫 번째 텍스트가 뷰어의 기본 서체로 출력됩니다

정렬과 맞춤을 위한 문자와 단어 간격

때로는 문자열은 올바르지만 너비가 잘못될 수 있습니다. 고정된 너비를 채워야 하는 헤더, 간격이 더 넓어야 보기 좋은 자릿수 코드, 정렬을 위해 값을 살짝 이동해야 하는 열 등이 이에 해당합니다. PDF에는 이를 위한 문자 간격(Tc, 모든 글리프 뒤에 추가되는 여백)과 단어 간격(Tw, 각 공백 문자(space)에 추가되는 여백)이라는 두 가지 텍스트 상태 연산자가 있습니다. 둘 다 스케일이 조정되지 않은 텍스트 공간 단위(효과적으로는 현재 글꼴 크기의 포인트)로 표현됩니다. 이는 TextOut의 인수가 아닌 상태(state)이므로 설정하고 그린 다음 다시 원래대로 설정해야 합니다

// Letter-space a short heading so it stretches across a rule.
Pdf.CurrentPage.SetCharacterSpacing(4);
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(72, 740, 0, 'S U M M A R Y');
Pdf.CurrentPage.SetCharacterSpacing(0);   // reset before normal body text

// Open up the gaps between words on a single wide line.
Pdf.CurrentPage.SetWordSpacing(6);
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(72, 712, 0, 'Name        Department        Extension');
Pdf.CurrentPage.SetWordSpacing(0);

단어 간격은 공백 문자(코드 32)에만 작용하므로 기억해 둘 가치가 있습니다. 즉, ASCII 공백이 없는 CJK 텍스트에서는 아무런 역할도 하지 않으며 바이트 대신 글리프 인덱스로 인코딩된 텍스트와는 묘하게 상호작용합니다. 라틴어 텍스트로 된 표 데이터 출력의 경우 단어 간격은 문자열을 다시 입력하지 않고도 간격을 넓힐 수 있는 편리한 방법입니다. 반면 목표 너비에 도달해야 하는 헤더의 경우, 문자 간격(Character spacing)이 더 적합한 도구인데 이는 조정치를 공백에 모아두는 것이 아니라 모든 글리프에 균일하게 분산시키기 때문입니다

설정 초기화는 매우 중요한 기본 원칙입니다. 간격 역시 글꼴과 마찬가지로 페이지의 그리기 상태(drawing state)에 속하며, 사용자가 상태를 변경할 때까지 계속 유지됩니다. 특정 헤더의 자간을 조정하고 다시 0으로 되돌리지 않으면 그 아래의 모든 단락이 확장된 속성을 물려받게 되어, 가벼운 교정에서는 놓칠 수 있지만 주의 깊게 보면 느껴지는 모호하고 불편한 오류를 남깁니다. 가장 확실한 습관은 간격 값을 설정하고, 필요한 텍스트를 그린 다음, 즉시 다음 줄에서 다시 0으로 설정하여 이후의 코드가 이전 섹션에서 한 작업을 알 필요가 없도록 하는 것입니다

수평 텍스트 스케일링, 문자 간격, 단어 간격, 채우기 대 선 렌더링 모드를 비교하는 HotPDF TextOut 페이지

실제 문제가 발생하는 곳에서 결과물 확인하기

텍스트 레이아웃은 첫 번째 기계가 아닌 두 번째 기계에서 실패합니다. 즉, 중요한 확인 작업은 개발자의 책상에서 멀리 떨어진 곳에서 발생합니다. 개발자용 글꼴이 설치되지 않은 시스템에서 생성된 파일을 열어 악센트가 있는 라틴어, 비라틴 문자 및 구두점을 포함하여 포함된 서체가 쉬운 문자를 무작위로 확인하는 것이 아니라 한 번에 제대로 렌더링되는지 확인해야 합니다. 몇 줄을 선택하고 복사하여 텍스트가 외곽선(outlines)이 아닌 실제 텍스트인지 확인합니다. 이는 검색 또는 추출이 필요한 순간 중요해집니다. 잘 정돈된 자리 표시자(placeholder) 대신 가장 긴 독일어 레이블이나 자릿수가 긴 숫자와 같이 레이아웃을 잘 나타낼 수 있는 데이터를 입력하세요. 필드를 넘치는 텍스트는 언제나 직접 입력하지 않은 문자열에서 비롯되기 때문입니다. 만약 페이지를 미리 인쇄된 양식 위에 겹쳐 인쇄해야 한다면, 샘플 하나를 인쇄하거나 래스터화(rasterize)하여 원본과 대조해보십시오. 화면에서는 눈에 띄지 않는 1/4 밀리미터의 기준선 벗어남 현상이 종이 위에서는 확연히 나타날 수 있습니다

아직 한 페이지도 작성해 보지 않으셨다면 문서, 글꼴, 그리고 위에서 언급한 모든 사항의 기반이 되는 좌측 하단 좌표계를 설정하는 HotPDF Hello 단어 예제부터 시작해 보세요. 여기에 표시된 TextOut, SetFont 및 간격 호출은 Delphi 및 C++Builder용 HotPDF 컴포넌트의 일부입니다