명세의 관점에서 쓴 첫 줄은 y = 770에 착지합니다. 종이 위에서는 그 추론이 그럴듯합니다. ISO 32000-1은 사용자 공간의 원점을 왼쪽 아래 모서리에 두고, A4 페이지는 높이가 842포인트이므로 770은 위쪽 가장자리에서 1인치 아래입니다. 그 프로그램을 위에서부터 재는 컴포넌트에 돌리면 인사말은 아래쪽 가장자리에서 1인치 위쯤에 찍히고 페이지의 나머지는 텅 비어 있습니다. 두 번째 페이지를 추가하면, 공들여 고른 글꼴은 다시 12포인트 Arial로 돌아옵니다. 두 놀라움은 모든 PDF 프로그램이 첫 페이지에서 내리는 같은 두 결정, 즉 어느 글꼴이 현재인지와 어느 모서리에서부터 세는지에서 나옵니다
Delphi에서 가장 작은 HotPDF 프로그램
HotPDF Hello World는 정해진 순서의 네 번 호출을 가진 콘솔 프로그램입니다. 문서 속성 설정, BeginDoc, SetFont와 TextOut으로 그리기, 그리고 EndDoc. 나머지는 평범한 Delphi입니다. 열한 개 언어로 인사하는 한 페이지를 쓰는 코드입니다:
program HelloWorld;
{$APPTYPE CONSOLE}
uses
Winapi.Windows, // DEFAULT_CHARSET용; pre-XE2 Delphi에서는 일반 Windows 사용
HPDFDoc; // HotPDF의 메인 unit
procedure CreateHelloWorld(const FileName: string);
var
Pdf: THotPDF;
begin
Pdf := THotPDF.Create(nil);
try
// 문서 전체 설정. 이것들은 모두 BeginDoc 전에
Pdf.FileName := FileName;
Pdf.AutoLaunch := False; // 기본값은 True. EndDoc 뒤 뷰어를 열어 보므로
Pdf.Compression := cmFlateDecode; // 기본값은 cmNone
Pdf.FontEmbedding := True; // 이미 기본값이지만, 읽는 사람을 위해 적어 둠
Pdf.EnableFontSubsetting := True; // 실제로 그린 글리프만 embed
Pdf.BeginDoc; // 페이지 1(A4)을 만듦
// 4번째 인수의 기본값은 ANSI_CHARSET. DEFAULT_CHARSET을 명시적으로 넘길 것
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
// X, Y는 왼쪽 위 모서리에서 포인트 단위. Y는 줄의 위쪽 가장자리
Pdf.CurrentPage.TextOut(72, 72, 0, 'Hello, Delphi PDF world!'); // English
Pdf.CurrentPage.TextOut(72, 102, 0, 'Hola, mundo Delphi PDF!'); // Spanish
Pdf.CurrentPage.TextOut(72, 132, 0, 'Hallo, Delphi PDF Welt!'); // German
Pdf.CurrentPage.TextOut(72, 162, 0, 'Bonjour, monde PDF Delphi!'); // French
Pdf.CurrentPage.TextOut(72, 192, 0, 'Ciao, mondo Delphi PDF!'); // Italian
Pdf.CurrentPage.TextOut(72, 222, 0, 'Olá, mundo Delphi PDF!'); // Portuguese
Pdf.CurrentPage.TextOut(72, 252, 0, 'Здравствуйте, Delphi PDF мир!'); // Russian
Pdf.CurrentPage.TextOut(72, 282, 0, 'こんにちは、Delphi PDFの世界!'); // Japanese
Pdf.CurrentPage.TextOut(72, 312, 0, 'Merhaba, Delphi PDF dünyası!'); // Turkish
Pdf.CurrentPage.TextOut(72, 342, 0, '你好,Delphi PDF世界'); // Chinese
Pdf.CurrentPage.TextOut(72, 372, 0, '여보세요, Delphi PDF 세계!'); // Korean
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
begin
CreateHelloWorld('HelloWorld.pdf');
Writeln('Wrote HelloWorld.pdf');
end.

try/finally ... Free와 짝을 이루는 THotPDF.Create(nil)은 평범한 Delphi 소유권 패턴이고, PDF를 만든다고 이 패턴이 바뀌지는 않습니다. 줄들은 Y = 72부터 30포인트 간격으로 내려갑니다. 14포인트 텍스트에 넉넉한 행간입니다. 실행하면 작업 디렉터리에 HelloWorld.pdf가 나타납니다. 한 페이지, 압축된 콘텐츠, 서브셋으로 임베드된 글꼴입니다
문서 속성은 BeginDoc 전에
BeginDoc은 문서를 시작하고 첫 페이지를 만들므로, 파일 전체를 결정하는 것들은 그 전에 할당돼야 합니다. HotPDF는 이를 관례에 맡기지 않고 일부 속성에서 강제합니다. BeginDoc 뒤에 Compression을 할당하면 세터는 Cannot set Compression value - document in progress.라는 메시지의 예외를 일으키고, AutoLaunch도 같은 방식으로 보호됩니다. 여기서는 시끄러운 게 좋습니다. 모든 문서 설정을 BeginDoc 위의 한 블록에 몰아 넣으면 그 메시지를 볼 일이 없습니다
기본값 중 둘은 다시 볼 만합니다. Compression은 cmNone으로 시작하므로, 손대지 않은 프로그램은 압축되지 않은 콘텐츠 스트림을 기록합니다. 텍스트 한 페이지라면 그저 낭비이지만, 수백 페이지 보고서라면 파일 크기에서 가장 먼저 눈에 띄는 것이 됩니다. AutoLaunch는 True로 시작하는데, 이는 콘솔 도구가 EndDoc이 돌아오는 순간 완성된 PDF를 연결된 뷰어로 연다는 뜻입니다. 책상에서는 기분 좋지만 서비스, 예약 작업, 빌드 에이전트 안에서는 틀린 동작입니다. 그런 환경이라면 코드에서 False로 설정하세요. 코드를 고칠 수 없는 CI 실행이라면 HOTPDF_SUPPRESS_AUTO_LAUNCH 환경 변수를 1로 두는 방법도 실행을 끕니다
글꼴 처리는 저희가 입장을 취하는 부분입니다. FontEmbedding은 기본값이 True라서 서체가 파일과 함께 다니고, Arial Unicode MS가 한 번도 설치되지 않은 머신에서도 렌더링됩니다. 반면 EnableFontSubsetting은 편집 워크플로를 위해 전체 글꼴을 남겨 두도록 기본 꺼져 있습니다. 생성되는 인사말, 청구서, 보고서라면 서브셋팅을 켜세요. Arial Unicode MS는 방대한 문자 범위를 커버하므로, 짧은 열한 줄을 보여 주려고 전체 서체를 임베드하면 눈에 보이는 이득 없이 파일이 메가바이트 단위로 커집니다. 누군가 다운스트림에서 텍스트를 편집하며 여러분이 그리지 않은 글리프를 필요로 할 때만 끄세요
HotPDF 페이지에서 (0, 0)은 어디인가
HotPDF는 페이지 좌표를 왼쪽 위 모서리에서부터 재며 Y는 아래로 증가합니다. VCL TCanvas와 같은 프레임입니다. 기본 Resolution 72에서 한 단위는 1포인트, 즉 1/72인치입니다. PDF 파일 자체는 ISO 32000-1 §8.3.2.3이 요구하는 대로 아래에서 위로 가는 좌표를 여전히 저장합니다. HotPDF는 콘텐츠 스트림을 쓸 때 여러분이 넘긴 모든 Y를 페이지 높이에서 빼서 변환합니다
도입부 예제의 산수는 짧습니다. BeginDoc과 AddPage가 만드는 기본 페이지는 A4, 595x842포인트입니다. 위에서 1인치 자리에 놓으려는 줄은 HotPDF 기준 Y = 72이고 raw PDF 기준 y = 770입니다. 842 - 72 = 770이니까요. 770을 TextOut에 넘기면 텍스트는 위에서 아래로 770포인트 내려가, 아래 가장자리에서 72포인트 위에 놓입니다
같은 호출에는 더 작은 함정이 하나 더 있습니다. 기본값에서 여러분이 넘긴 Y는 텍스트의 위쪽 가장자리이고, HotPDF는 베이스라인을 놓기 전에 텍스트 높이만큼 내려갑니다. GDI에서 포팅한 코드는 보통 정확히 그것을 기대합니다. 다른 PDF 라이브러리에서, 또는 여러분 자신의 콘텐츠 스트림 작성기에서 포팅한 코드는 보통 Y가 베이스라인이라 기대하고, 모든 줄이 계획보다 텍스트 높이 하나만큼 낮게 나옵니다. THPDFPage.TextFromBaseLine(True)가 현재 페이지를 베이스라인 배치로 바꿉니다:
// 아래에서 위로 올라오는 좌표계(PDF 레이아웃, 명세 다이어그램, 다른 라이브러리)에서
// 오는 좌표를 위한 다리 역할. 이 글을 위해 쓴 헬퍼이며 HotPDF의 일부가 아님.
// 기본 Resolution 72에서 유효
function FromBottomUp(Page: THPDFPage; PdfY: Single): Single;
begin
Result := Page.Height - PdfY;
end;
procedure DrawFromPdfCoordinates(Pdf: THotPDF);
begin
Pdf.CurrentPage.SetFont('Arial', [], 12);
// 이제 Y는 베이스라인. PDF 자체 텍스트 연산자가 글리프를 놓는 방식
Pdf.CurrentPage.TextFromBaseLine(True);
// 바닥 모서리에서 770포인트 위의 베이스라인. 즉 위에서 Y = 72
Pdf.CurrentPage.TextOut(72, FromBottomUp(Pdf.CurrentPage, 770), 0,
'Baseline one inch below the top edge');
end;
Resolution은 숫자의 의미를 바꾸는 또 다른 설정입니다. THotPDF.Resolution을 96으로 두면 X/Y 좌표, 여백, TextWidth 결과가 1/96인치 단위로 표현됩니다. 레이아웃을 화면 픽셀로 설계했다면 요긴합니다. 하지만 페이지 Width와 Height는 여전히 포인트를 보고하므로, 페이지 경계와 드로잉 좌표를 섞어 쓰는 레이아웃 코드는 X, Y와 같은 단위로 돌아오는 CurrentPage.UserWidth와 CurrentPage.UserHeight를 읽어야 합니다. 72에서는 두 쌍이 일치합니다. 밀리미터로 생각하고 싶다면 MeasurementUnit 속성(기본값 muPoints, muMM, muInches, muCentimeters, muTwips, muPixels 사용 가능)이 더 깔끔한 스위치입니다. 프로그램마다 하나의 관례를 고르세요. 절대 겹쳐 쓰지 마세요
2페이지는 왜 12포인트 Arial로 찍힐까?
모든 새 페이지는 알려진 상태에서 시작하기 때문입니다. THotPDF.AddPage는 새 페이지를 현재로 만들고, A4로 크기를 정하고, 그 페이지에 SetFont('Arial', [], 12)를 호출하며, Resolution과 무관하게 크기를 12포인트로 유지합니다. BeginDoc도 같은 경로로 1페이지를 만들므로, Hello World가 글꼴을 BeginDoc 직후에 설정하는 것이지 그 전이 아닌 이유가 여기에 있습니다. 다중 페이지 출력에서 놓치기 쉬운 결과는 이것입니다. 1페이지에서 호출한 SetFont는 이어지지 않습니다. 2페이지의 라틴 줄은 여전히 Arial로 멀쩡해 보입니다. 그저 작을 뿐이죠. CJK 줄이 결정적 증거입니다. Arial에는 그 글리프가 없어서 리더는 빈 상자를 보여 주거나 아무것도 보여 주지 않습니다
예측 가능한 시작 글꼴은 페이지 객체로서 올바른 설계입니다. 5페이지용 드로잉 코드가 4페이지가 남긴 상태에 조용히 의존해서는 안 되니까요. 비용은 페이지 설정을 반복해야 한다는 것이고, 가장 깔끔한 반복 방법은 첫 페이지를 포함해 모든 페이지가 거치는 단일 프로시저입니다:
uses
Winapi.Windows, Vcl.Graphics, HPDFDoc;
// 여러분만의 헬퍼. 새 페이지에 무엇이 필요한지 아는 한 곳
procedure PreparePage(Pdf: THotPDF; IsFirst: Boolean);
begin
if not IsFirst then
Pdf.AddPage; // 글꼴을 Arial 12로 리셋
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
end;
procedure WriteGreetings(Pdf: THotPDF; Lines: array of string);
const
TopMargin = 72;
Leading = 30;
LinesPerPage = 20;
var
I, Row: Integer;
begin
PreparePage(Pdf, True); // BeginDoc 뒤에는 페이지 1이 이미 존재
Row := 0;
for I := 0 to High(Lines) do
begin
if Row = LinesPerPage then
begin
PreparePage(Pdf, False);
Row := 0;
end;
Pdf.CurrentPage.TextOut(72, TopMargin + Row * Leading, 0, Lines[I]);
Inc(Row);
end;
// 굵은 제목에는 SetFont가 따로 필요하고, 본문 글꼴은 되돌아온다
Pdf.AddPage;
Pdf.CurrentPage.SetFont('Arial Unicode MS', [fsBold], 18, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 72, 0, 'Appendix');
Pdf.CurrentPage.SetFont('Arial Unicode MS', [], 14, DEFAULT_CHARSET);
Pdf.CurrentPage.TextOut(72, 110, 0, 'Body text after the heading');
end;
페이지가 TextFromBaseLine으로 베이스라인 배치까지 전환한다면 그 호출도 글꼴 옆에 PreparePage 안에 넣으세요. 페이지별 설정을 한 프로시저에 모아 두면 새 요구사항, 색, 다른 크기가 한 번만 추가돼 모든 페이지에 적용됩니다
다양한 문자를 위한 하나의 글꼴, 그리고 한계
하나의 SetFont가 같은 페이지에서 라틴, 키릴, 일본어, 중국어를 모두 운반할 수 있는 것은 Arial Unicode MS가 방대한 문자 범위의 글리프를 하나의 서체에 묶어 두기 때문입니다. SetFont의 네 번째 인수는 Windows 문자 세트이며 기본값은 ANSI_CHARSET인데, 이것은 글꼴 선택을 서부 코드 페이지에 묶습니다. DEFAULT_CHARSET(값 1)을 넘기면 Windows가 이름으로 서체를 고르고, 다국어 페이지가 원하는 것이 바로 그것입니다
그 글꼴은 PDF를 빌드하는 머신에 존재해야 합니다. HotPDF는 Windows가 이름을 해석한 결과인 글꼴을 임베드하고, Arial Unicode MS가 없으면 Windows는 아무 불평 없이 다른 서체로 대체합니다. 그 출력은 대체 서체를 임베드하며, 그 서체가 커버하지 않는 문자의 줄은 빈 상자로 나옵니다. 이것이 드러나는 전형적인 장소는 빌드 서버입니다. 아무도 Office 글꼴을 설치해 두지 않으니까요. 배치 실행을 맡기기 전에 새 머신이 만드는 첫 파일의 임베드된 글꼴 목록을 확인하세요
하나의 서체가 모든 문자열을 커버하지 않을 때는 TextOutWithFallback이 TextOut과 같은 인수를 받아 문자마다 글꼴을 해석하고, 각 런을 그것을 렌더링할 수 있는 서체로 그린 뒤 원래 글꼴을 복원합니다. 아랍어, 히브리어 같은 오른쪽에서 왼쪽으로 쓰는 문자는 글리프 커버리지 이상이 필요합니다. 셰이핑과 뒤집힌 런 순서가 필요하고, 그것은 RtLTextOut 함수의 영역이며 보통 RegisterUnicodeTTF로 불러온 TrueType 파일과 짝을 이룹니다
여기서부터 자연스러운 다음 단계는 텍스트의 크기, 스타일, 회전이고 한 페이지에 여러 블록을 의도적으로 배치하는 것인데, 이는 TextOut 샘플이 다룹니다. 선, 상자, 이미지는 같은 CurrentPage 객체와 같은 왼쪽 위 좌표를 쓰므로, 원점과 페이지별 설정에 관한 위의 모든 이야기가 그대로 적용됩니다
이 Hello World가 여러분의 글꼴로 깨끗하게 돌아가는 것을 확인한 뒤에는, HotPDF Delphi Component 페이지가 도형과 이미지부터 폼까지 Delphi와 C++Builder용 드로잉 API의 나머지를 다룹니다