기술 문서

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

(72, 72)에 그린 문자열은 페이지 위쪽 가장자리에서 1인치 아래에 착지합니다. 콘텐츠 스트림 쪽에서 PDF를 배운 사람이라면 이게 함정입니다. 그쪽 좌표계에서는 y = 72가 바닥에서 1인치 위니까요. 두 번째 깜짝 사례는 몇백 줄 뒤에 나타납니다. US Letter로 설계한 보고서의 첫 페이지는 612 x 792 포인트로 나오는데 이후 모든 페이지는 595 x 842로 나옵니다. 용지 크기를 정하는 그 줄이 정확히 한 번만 실행됐기 때문입니다

두 현상의 뿌리는 같습니다. PDF의 텍스트는 상태 변경의 짧은 나열(글꼴과 크기, 문자 간격, 단어 간격, 위치와 회전을 위한 텍스트 행렬) 뒤에 show-text 연산자 하나가 따라오는 구조이고, 고수준 텍스트 호출은 그 나열을 감싼 얇은 래퍼입니다. 호출은 상태를 읽을 뿐 절대 대신 리셋해 주지 않습니다. 새 페이지는 이전 페이지가 끝난 상태가 아니라 고정된 기본값 세트에서 시작합니다

TextOut이 페이지의 어디에 (X, Y)를 찍는가

HotPDF는 THPDFPage.TextOut(X, Y: Single; angle: Extended; Text: WideString)로 텍스트를 그리고, Pdf.CurrentPage.TextOut으로 닿습니다. X와 Y는 페이지 왼쪽 위 모서리에서 잰 값이고 Y는 아래로 커지며, 기본 Resolution인 72에서는 한 단위가 1포인트, 즉 1/72인치입니다. 페이지는 각 Y를 페이지 높이에서 빼서 아래에서 위로 흐르는 PDF 사용자 공간으로 변환합니다. angle은 도 단위이고, 텍스트가 WideString이므로 유니코드는 그대로 통과합니다. 두 번째 오버로드는 이미 UTF-16 버퍼를 갖고 있는 코드를 위해 PWORD와 길이를 받습니다

페이지 지오메트리도 고정돼 있습니다. BeginDoc이 첫 페이지를 추가하고, BeginDoc이나 AddPage가 만드는 모든 페이지는 A4, 595 x 842 포인트, 현재 글꼴 Arial 12로 시작합니다. US Letter(612 x 792)는 CurrentPage.Size := Letter 할당 한 번이면 되지만, 이 할당은 페이지에 속하므로 새 페이지마다 반복해야 합니다

기본값에서 넘기는 Y는 텍스트의 꼭대기이고 베이스라인은 글꼴 어센트 하나만큼 아래에 깔립니다. TextFromBaseLine(True)이 페이지를 베이스라인 위치 잡기로 바꿉니다. 한 줄에 크기를 섞는 모든 경우에 켜 두세요. top-edge 모드에서는 14포인트 금액 옆의 9포인트 통화 코드가 꼭대기를 금액의 꼭대기에 맞추므로 눈에 띄게 뜨지만, 베이스라인 모드에서는 조판공이 심은 것처럼 두 런이 같은 줄에 앉습니다. 다음 예제의 마지막 블록이 정확히 그렇게 합니다

크기와 스타일은 SetFont에 산다

TextOut에는 크기나 스타일 매개변수가 없습니다. SetFont(FontName, FontStyle, ASize, FontCharset, IsVertical)가 그 모두를 책임지는데, 마지막 두 인수는 ANSI_CHARSET과 가로 텍스트가 기본이며, SetFont가 정한 것은 다음 SetFont가 올 때까지 뒤따르는 모든 TextOut에 유효합니다. 첫날 혼란의 대부분이 여기로 거슬러 올라갑니다. 어떤 줄이 굵게 나오는 건 세 호출 전에 무언가가 [fsBold]를 설정하고 아무도 지우지 않았기 때문입니다

var
  W: Single;
begin
  Pdf.BeginDoc;                                     // 첫 페이지: A4, Arial 12
  Pdf.CurrentPage.TextFromBaseLine(True);           // 아래의 모든 Y는 베이스라인

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

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

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

  Pdf.CurrentPage.SetFont('Courier New', [fsBold, fsUnderline], 10);
  Pdf.CurrentPage.TextOut(72, 136, 0, '+18.4% YoY');             // 스타일 결합

  // 한 베이스라인에 섞인 크기: 14 pt 서체가 current인 동안 측정
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  W := Pdf.CurrentPage.GetWideTextWidth('1,284.50');
  Pdf.CurrentPage.TextOut(72, 160, 0, '1,284.50');
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(72 + W + 3, 160, 0, 'EUR');            // 3 pt 간격
  ...
end;

스타일 인수는 평범한 TFontStyles 집합이므로 []은 일반, [fsBold, fsItalic]은 굵은 기울임입니다. fsUnderline과 fsStrikeOut은 런 아래나 런을 가로지르는 얇은 채운 막대로 그려지고, 런이 회전하면 함께 따라갑니다. 크기는 좌표와 같은 단위인 포인트라서 수직 계산이 단순합니다. 11포인트 본문을 16포인트 스텝으로 놓으면 줄 사이에 공기가 약 5포인트 남습니다. 자동 줄 진행은 없습니다. 모든 Y를 직접 계산해야 하는데, 문단에는 번거롭지만 필드가 고정 위치에 앉는 양식에는 정확히 맞는 방식입니다

GetWideTextWidth는 호출하는 순간 current인 글꼴로 측정하므로, 마지막 블록에서 측정은 14포인트 굵은 서체가 아직 설정된 동안에 일어나야 합니다. 두 번째 SetFont 아래로 옮기면 9포인트 접미사가 금액의 마지막 자릿수 위에 얹힙니다

글꼴 이름은 PDF를 빌드하는 기계에 설치된 글꼴을 기준으로 확인되므로, 개발자 데스크톱에서 확인되는 서체가 빌드 서버에는 없을 수 있고 완성된 파일의 서체는 어느 기계가 만들었는지에 따라 달라집니다. 문자 체계 커버리지가 나머지 절반입니다. 라틴 전용 서체에 키릴이나 CJK를 그리게 하면 글리프 없는 상자가 나오는데, 이건 호출의 속성이 아니라 글꼴 파일의 속성입니다. 하나의 문자열에 어느 단일 서체도 커버하지 못하는 문자 체계가 섞일 때는 TextOutWithFallback이 같은 네 인수를 받아 기본 글꼴에 없는 문자를 런마다 서체를 바꿔 그리고, 끝나면 원래 글꼴을 복원합니다

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

angle 인수는 텍스트를 어떻게 회전시킬까?

0이 아닌 각도는 런을 앵커 중심으로 그 각도만큼 시계 반대 방향으로 돌리고, 앵커는 텍스트 자신의 프레임에서 텍스트의 왼쪽 위 모서리입니다. 앵커는 움직이지 않습니다. 글리프가 행진하는 방향만 바뀌므로, 수평 레이블을 놓인 좌표가 회전한 쌍둥이도 놓입니다. 회전한 런은 페이지가 베이스라인 모드여도 그 왼쪽 위 앵커를 유지하며, TextFromBaseLine은 수평 런만 옮깁니다

HotPDF TextOut 회전 지오메트리 다이어그램: 0, 45, 90도의 런이 고정 앵커 중심으로 시계 반대 방향으로 돌아가고, 책등 레이블, 워터마크, 기울인 열 헤더의 Delphi TextOut 호출 예시
0이 아닌 각도는 움직이지 않는 앵커, 즉 텍스트의 왼쪽 위 모서리 중심으로 런을 시계 반대 방향으로 돌리고, 베이스라인을 공유하는 런들은 가로 줄이 Y를 밟는 방식 그대로 X를 전진시킵니다
var
  W, AxisMidY: Single;
begin
  AxisMidY := 400;                                // 차트 Y축의 중앙

  // 90도 런은 아래에서 위로 읽히며 Y에서 위로 자라므로,
  // 축 중앙에 맞추려면 폭의 절반만큼 중앙 아래에서 시작해야 합니다
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 11);
  W := Pdf.CurrentPage.GetWideTextWidth('Units sold');
  Pdf.CurrentPage.TextOut(40, AxisMidY + W / 2, 90, 'Units sold');

  // 대각선 워터마크
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 60);
  Pdf.CurrentPage.TextOut(150, 250, 45, 'DRAFT');

  // 60도 기울인 열 헤더: 같은 Y, X 이동
  Pdf.CurrentPage.SetFont('Arial', [], 9);
  Pdf.CurrentPage.TextOut(120, 600, 60, 'Q1 actual');
  Pdf.CurrentPage.TextOut(160, 600, 60, 'Q2 actual');
end;

90도가 축 레이블과 책등 제목을 커버합니다. 90도에서 글리프는 앵커의 X 오른쪽에 한 줄 높이 폭으로 서고, 런은 자기 폭만큼 페이지 꼭대기를 향해 올라갑니다. 중앙 맞춤이 폭을 뺄 게 아니라 Y에 반 폭을 더하는 이유이고, 중점 자체에 앵커를 둔 레이블이 전부 그 위에 앉는 이유이기도 합니다. 60도는 열 헤더에 어울립니다. 긴 레이블이 좁은 열 위에 서면서 옆 열로 쏟아지지 않게 해 주죠. 45도는 흔한 워터마크 각도입니다. 베이스라인을 공유하는 런들은 같은 Y와 계단식 X를 받는데, 쌓아 올린 가로 줄들이 X를 공유하고 Y를 밟는 것과 정확히 같은 방식입니다

두 번째 페이지는 왜 Arial 12의 A4로 나올까?

AddPage는 새 페이지의 인덱스를 반환하며 그 페이지를 current로 만듭니다. 새 페이지는 기본값에서 시작합니다. A4, Arial 12, top-edge 위치 잡기, 문자·단어 간격 0입니다. 이전 페이지의 어떤 것도 넘어오지 않습니다. PDF 페이지의 콘텐츠 스트림은 어차피 새 그래픽스 상태로 시작하므로 이 설계는 각 페이지를 자기 완결적으로 유지하지만, 문서 맨 위에서 페이지를 한 번만 설정하는 레이아웃 코드는 2페이지에서 조용히 용지 크기와 서체를 바꿔 버린다는 뜻이기도 합니다. 해결책은 페이지별 설정을 모두 한 루틴에 모아 BeginDoc 뒤와 모든 AddPage 뒤에 호출하는 것입니다

HotPDF: US Letter 페이지 다이어그램. 왼쪽 여백, 상단 시작 줄, 아래 여백 한계를 향해 고정 Leading으로 내려가는 줄 위치 산술과, 수동 AddPage 가드와 SetFont 리셋을 갖춘 이름 상수 레이아웃 루프
이름 있는 앵커는 매직 넘버 열을 산술로 바꿉니다. 매 줄 실행 Y를 Leading만큼 전진시키고, 아래 여백은 AddPage로 직접 지킵니다
procedure StartReportPage(Pdf: THotPDF);
begin
  Pdf.CurrentPage.Size := Letter;               // 새 페이지는 A4로 시작
  Pdf.CurrentPage.TextFromBaseLine(True);       // 새 페이지는 top-edge 모드로 시작
  Pdf.CurrentPage.SetFont('Arial', [], 11);     // 새 페이지는 Arial 12로 시작
end;

procedure WriteReport(Pdf: THotPDF; Lines: TStrings);
const
  LeftMargin   = 72;   // 1인치
  TopLine      = 72;   // 첫 베이스라인, 1인치 아래
  BottomMargin = 72;
  Leading      = 16;
var
  I: Integer;
  Y: Single;
begin
  StartReportPage(Pdf);
  Y := TopLine;
  for I := 0 to Lines.Count - 1 do
  begin
    if Y > Pdf.CurrentPage.Height - BottomMargin then   // Letter에서 720
    begin
      Pdf.AddPage;
      StartReportPage(Pdf);
      Y := TopLine;
    end;
    Pdf.CurrentPage.TextOut(LeftMargin, Y, 0, Lines[I]);
    Y := Y + Leading;
  end;
end;

TextOut 아래에는 플로우 레이아웃이 없고, 페이지 바닥을 넘은 Y도 콘텐츠 스트림 관점에서는 여전히 유효한 좌표입니다. 텍스트는 기록되지만 미디어 박스 밖에 떨어져 어떤 뷰어도 보여 주지 않을 뿐이죠. 그래서 루프는 매 줄 전에 Y를 검사하고 스스로 새 페이지를 엽니다. 720을 하드코딩하는 대신 한계를 CurrentPage.Height에서 읽으면, 나중에 누군가 헬퍼를 A4로 되돌려도 루프는 옳게 유지됩니다. 같은 1인치 여백이라도 A4에서는 한계가 770이 됩니다

가장 빠른 확인은 뷰어 인쇄 대화상자가 하는 바로 그것입니다. 출력물을 열어 2페이지로 간 뒤 문서 속성을 보세요. Letter 페이지는 8.5 x 11인치, A4 페이지는 약 8.27 x 11.69로 보고합니다. 첫 페이지와 둘째 페이지가 다르다면 페이지별 설정이 헬퍼 밖에 살아 있는 것입니다

문자 간격과 단어 간격도 페이지 상태다

가끔은 문자열은 맞고 폭이 틀립니다. 고정된 룰을 걸쳐야 하는 헤딩, 자릿수가 느슨해야 읽기 좋은 코드, 간격을 벌려야 하는 레이블 줄이 그렇죠. PDF에는 ISO 32000-1에 이를 위한 텍스트 상태 매개변수가 두 개 있습니다. 모든 글리프 뒤에 더해지는 문자 간격 Tc(§9.3.2)와 각 공백에서 더해지는 단어 간격 Tw(§9.3.3)입니다. HotPDF는 SetCharacterSpacing(Spacing: Single)과 SetWordSpacing(Spacing: Single)으로 이들을 설정하고, 기본 해상도 기준 포인트 단위이며, 글꼴처럼 바꿀 때까지 유효합니다

HotPDF: 문자 간격 Tc가 SUMMARY의 모든 글리프 뒤에 여분 공간을 고르게 펼치는 것과 단어 간격 Tw가 공백 문자에만 모으는 것의 비교. 간격 상태가 뒤 문단으로 새지 않게 하는 set, draw, reset 사이클에 연결된 모습
Tc는 조정 값을 모든 글리프에 펼치고 Tw는 공백 문자에만 작용하며, 다음 줄에서 리셋하면 그리기 상태가 깨끗하게 유지됩니다

단어 간격에는 규격에 적혀 있는 함정이 하나 있습니다. §9.3.3은 Tw를 1바이트 문자 코드 32에만 적용하고, 명시적으로 다중 바이트 코드 안의 바이트 32에는 적용하지 않습니다. 그래서 연산자 그 자체로는 2바이트 글리프 코드로 기록된 텍스트에 아무 효과가 없는데, 임베디드 유니코드 글꼴은 보통 그렇게 기록됩니다. HotPDF도 유니코드 런을 그렇게 쓰고, 그 경로에서는 단어 간격을 각 공백 글리프 뒤의 명시적 위치 조정으로 적용하므로 SetWordSpacing은 어느 쪽이든 간격을 벌립니다. 트리거는 여전히 평범한 공백, U+0020입니다. ASCII 공백이 없는 중국어나 일본어 런은 전혀 여유를 받지 못하고, 표의문자 공백(U+3000)은 평범한 글리프로 취급됩니다

헤딩이 목표 폭에 도달해야 한다면 문자 간격이 맞는 도구입니다. 변화를 공백에 몰아 붙이지 않고 고르게 펼치기 때문입니다. GetWideTextWidth는 현재 유효한 문자·단어 간격을 반영하므로, 둘 다 0으로 둔 상태에서 측정한 뒤 간격을 유도하세요:

const
  Heading = 'SUMMARY';
  Target  = 200;        // 헤딩이 걸쳐야 할 룰의 폭, 포인트 단위
var
  Natural, Tc: Single;
begin
  Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // 남은 간격 없이 측정
  Natural := Pdf.CurrentPage.GetWideTextWidth(Heading);

  // Tc는 모든 글리프 뒤에 붙지만, 글리프 사이 간격만 눈에 보입니다
  Tc := (Target - Natural) / (Length(Heading) - 1);

  Pdf.CurrentPage.SetCharacterSpacing(Tc);      // 음수 값은 조입니다
  Pdf.CurrentPage.TextOut(72, 72, 0, Heading);
  Pdf.CurrentPage.SetCharacterSpacing(0);       // 본문 텍스트 전에 리셋

  Pdf.CurrentPage.SetWordSpacing(6);
  Pdf.CurrentPage.SetFont('Arial', [], 11);
  Pdf.CurrentPage.TextOut(72, 100, 0, 'Name Department Extension');
  Pdf.CurrentPage.SetWordSpacing(0);
end;

리셋 줄이 설정 줄보다 더 중요합니다. 헤딩 하나 뒤에 4포인트 문자 간격을 남겨 두면 아래의 모든 문단이 그 늘어짐을 물려받고, 가벼운 교정으로는 걸러지지 않는 애매한 위화감으로 살아남다 인쇄본에서 드러납니다. 값을 설정하고, 필요한 단 하나의 런을 그리고, 다음 줄에서 0으로 되돌리세요. 그러면 뒤의 코드는 앞 절이 무슨 짓을 했는지 알 필요가 없습니다. 같은 페이지는 높이를 바꾸지 않고 런을 조이는 SetHorizontalScaling(Tz, 기본 100)과 외곽선 텍스트를 위한 trStroke 같은 값의 SetTextRenderingMode도 제공하며, 둘 다 정확히 같은 방식으로 지속됩니다

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

이번이 첫 출력 페이지라면 HotPDF Hello World 예제가 문서 설정과 저장을 다룹니다. 열 전체를 꽉 채워야 하는 텍스트라면 PDF 텍스트의 완전 정렬에 자체 워크스루가 있고, 위아래로 흐르는 CJK는 HotPDF의 세로 텍스트 레이아웃에서 다룹니다. 위의 TextOut, SetFont, 간격 호출, 페이지 크기 설정은 모두 Delphi와 C++Builder용 HotPDF Delphi Component에 들어 있습니다