기술 문서

델파이에서의 HotPDF Canvas 그리기: 벡터 경로와 색상

PDF 콘텐츠 스트림 예제에서 가져온 꼬리 규칙선은 72 72 m 540 72 l S입니다. (72, 72)로 이동해 (540, 72)까지 선을 긋고 외곽선을 칩니다. 날 PDF에서 이 선은 US Letter 페이지 아래 모서리에서 1인치 위에 놓입니다. 같은 네 숫자를 좌상단 모서리에서 잰 그리기 API에 넘기면 규칙선은 상단에서 1인치 아래, 편지지 머리글을 관통해 내려앉습니다. 선 자체는 멀쩡합니다. 기준 프레임이 바뀌었을 뿐이고, 792포인트 페이지에서 두 프레임 사이의 이동은 792 빼기 Y, 단 한 번의 뺄셈입니다

HotPDF 페이지에서 Y = 0은 어디인가?

HotPDF 페이지 드로잉 호출은 원점을 페이지의 좌상단 모서리에 두고 X는 오른쪽으로, Y는 아래로 증가합니다. VCL TCanvas와 같은 프레임입니다. ISO 32000-1 §8.3.2.3에 정의된 PDF 사용자 공간은 좌하단에서 Y가 위로 증가하며, HotPDF는 콘텐츠 스트림을 쓸 때 여러분이 넘기는 모든 Y를 변환합니다. 그러므로 THotPDF.CurrentPage의 Y := 72는 페이지 크기와 무관하게 상단 모서리에서 1인치 아래를 뜻합니다

페이지 크기는 다른 쪽 끝에서 문제가 됩니다. 612 × 792포인트 US Letter 페이지에서 Y := 720은 아래에서 1인치 위입니다. 새 HotPDF 페이지는 별도 지시가 없으면 A4이고 A4는 842포인트라서 같은 Y := 720 아래에 122포인트, 약 1.7인치가 남습니다. 792에서 빼서 잡은 푸터가 A4에서 위로 뜨는 건 정확히 이 이유입니다. 하드코딩 대신 CurrentPage.Height에서 높이를 읽거나 BeginDoc 직후 CurrentPage.Size를 명시적으로 설정하세요

VCL TCanvas와 HotPDF 페이지 드로잉 API의 비교. 둘 다 좌상단에서 잰 채로 Y가 아래로 증가하는 반면, HotPDF는 US Letter 페이지 상단 근처의 같은 점을 하단 기준 PDF 사용자 공간의 y = 720으로 기록합니다
HotPDF는 TCanvas와 같은 좌상단 프레임에서 그리므로 612 × 792 페이지 상단에서 1인치 아래 지점은 양쪽에서 Y = 72이고, HotPDF는 그것을 PDF 사용자 공간 y = 720으로 파일에 기록합니다

단위는 포인트, 즉 1/72인치입니다. THotPDF.Resolution의 기본값이 72이기 때문입니다. Resolution을 올리면 좌표와 크기가 대신 1/Resolution인치 단위로 계산됩니다. 벡터 작업이라면 72에 두세요. 선 두께, 대시 길이, 좌표가 모두 PDF 파일이 저장하는 단위로 읽히고, 콘텐츠 스트림에서 본 값이 소스의 값과 일치합니다

텍스트도 같은 모델을 따릅니다. 기본적으로 TextOut에 주는 Y는 텍스트의 상단 모서리라서, 레이블과 그 주위 상자를 하나의 격자 위에서 설계합니다. 몇몇 호출은 그리기 표면이 아니라 PDF 파일을 대상으로 하위 PDF 좌표를 씁니다. SetCropBox(Left, Bottom, Right, Top) 같은 페이지 상자 설정자와, 이미 로드된 문서에 도장을 찍는 StampLoadedPageText / StampLoadedPageImage 계열이 그렇습니다. 뒤집힌 레이아웃의 대부분은 이것들과 경로 호출을 섞을 때 나오므로 별도 루틴에 격리하세요

경로 만들기와 칠하기

경로는 먼저 만들어지고 나중에 칠해집니다. MoveTo는 아무것도 표시하지 않은 채 시작점을 잡고, LineTo마다 직선 구간이 더해지며, Stroke 같은 칠하기 호출이 현재 선 색과 선 두께로 소비하기 전까지는 아무것도 페이지에 닿지 않습니다. PDF 용어로 Stroke은 S 연산자를 내보내고, 모든 칠하기 연산자는 현재 경로를 끝내므로 다음 MoveTo는 깨끗이 시작합니다

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;
    Pdf.CurrentPage.Size := Letter;    // 612 x 792 pt; 기본값은 A4

    // 두께와 색은 페이지 상태: 한 번 설정하면 이후 모든 Stroke가 사용
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // 상단에서 1인치 아래 규칙선, 양쪽 72 pt 마진
    Pdf.CurrentPage.MoveTo(72, 72);
    Pdf.CurrentPage.LineTo(540, 72);
    Pdf.CurrentPage.Stroke;

    // 첫머리의 푸터 규칙선: 아래에서 1인치 위
    Pdf.CurrentPage.MoveTo(72, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.LineTo(540, Pdf.CurrentPage.Height - 72);
    Pdf.CurrentPage.Stroke;

    // 하나의 경로에 이어진 세 구간, 더 두껍고 파랑
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 152);
    Pdf.CurrentPage.LineTo(172, 102);
    Pdf.CurrentPage.LineTo(272, 172);
    Pdf.CurrentPage.LineTo(372, 112);
    Pdf.CurrentPage.Stroke;

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

선 두께는 인수가 아니라 상태입니다. SetLineWidth는 w 연산자를 쓰고 페이지의 모든 이후 스트로크는 바꿀 때까지 그 두께를 씁니다. 폴리라인이 3포인트로 나오고 두 규칙선은 1.5에 머무는 이유입니다. 설정하지 않으면 PDF 기본값 1.0이 적용됩니다(ISO 32000-1 §8.4.3.2). 사라진 도형의 원인은 더 단조롭습니다. 칠하기 호출 없이 놓이기만 한 기하는 렌더링되지 않습니다

좌표와 두께는 Single이라 72.25도 0.5포인트 헤어라인도 파일에 살아남습니다. 0이 특수 사례입니다. 사양은 너비 0을 출력 장치가 그릴 수 있는 가장 얇은 선으로 정의하는데, 화면에서는 어떤 확대 배율이든 1픽셀이고 600 dpi 프린터에서는 1/600인치입니다. 같은 규칙선이 뷰어에서는 굵어 보이고 종이에서는 거의 사라지는 이유죠. 헤어라인에는 0.25나 0.5 같은 실제 두께를 택하세요. 그래야 모든 장치에서 무게를 유지합니다

HotPDF 경로 모델: MoveTo, LineTo, Rectangle, Circle이 메모리에 보이지 않는 현재 경로를 만들고, Stroke, Fill, FillAndStroke 커밋만이 선 색, 채우기 색, 선 두께의 지속 그래픽 상태로 버퍼를 비우기 전에 칠합니다
기하는 칠하기 연산자가 저장된 선 색, 채우기 색, 선 두께로 커밋할 때까지 현재 경로에 조용히 쌓입니다. 칠하기 호출을 잊으면 도형은 그려지지 않은 채 남습니다

닫힌 삼각형 한 모서리에 홈이 파인 이유는?

시작점으로 돌아오는 LineTo 세 번으로 그린 삼각형은 닫혀 보이지만 PDF에서는 여전히 열린 서브패스입니다. ISO 32000-1 §8.5.3.2는 라인 조인을 서브패스 내부에서 구간이 만나는 지점에만 적용하고, 풀려난 두 끝은 라인 캡을 받습니다. 기본 butt 캡과 6포인트 펜이라면 시작 모서리는 다른 두 모서리의 깔끔한 미터와 달리 사각 홈을 드러냅니다. ClosePath(h 연산자)는 마지막 구간을 덧붙이고 렌더러에게 그것을 첫 구간에 조인하라고 알립니다

Pdf.CurrentPage.SetLineWidth(6);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

// 닫혀 보이지만 아닙니다: butt 캡이 (72, 500)에서 만나 홈을 남깁니다
Pdf.CurrentPage.MoveTo(72, 500);
Pdf.CurrentPage.LineTo(132, 420);
Pdf.CurrentPage.LineTo(192, 500);
Pdf.CurrentPage.LineTo(72, 500);
Pdf.CurrentPage.Stroke;

// 닫힌 서브패스: 세 모서리 모두 같은 미터 조인을 받습니다
Pdf.CurrentPage.MoveTo(232, 500);
Pdf.CurrentPage.LineTo(292, 420);
Pdf.CurrentPage.LineTo(352, 500);
Pdf.CurrentPage.ClosePath;
Pdf.CurrentPage.Stroke;              // 또는 한 번의 호출로 ClosePathStroke

// Fill에는 ClosePath가 필요 없습니다: 채우기는 모든 서브패스를 암묵적으로 닫습니다
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.MoveTo(392, 500);
Pdf.CurrentPage.LineTo(452, 420);
Pdf.CurrentPage.LineTo(512, 500);
Pdf.CurrentPage.Fill;

스트로크할 다각형은 마지막 점이 이미 첫 점과 같더라도 ClosePath나 ClosePathStroke로 닫으세요. 1포인트에서 홈은 화면에 안 보이지만 6포인트면 보통 확대에서 보이고, 인쇄된 차트 범례에서는 렌더링 결함으로 읽힙니다. 채우기 도형에서는 질문 자체가 생기지 않고, 경로가 자기 교차할 때 0이 아닌 굴곡 대신 짝수-홀수 규칙을 원하면 EoFill이 그 자리에 있습니다

사각형, 원, TCanvas 습관

닫힌 원시 도형은 MoveTo 장부 일을 덜어 줍니다. Rectangle(X, Y, Width, Height)는 좌상단 모서리와 크기를 받고, Circle(X, Y, Radius)는 중심과 반지름을 받으며, 어느 쪽이든 Fill, Stroke, FillAndStroke로 칠해집니다. 채우기 색과 선 색은 독립입니다. SetRGBFillColor와 SetRGBStrokeColor는 각각 TColor를 받으므로 Delphi 상수와 RGB(...)가 곧장 통하고, SetRGBColor는 둘 다 한 번에 설정합니다

Rectangle의 인수 모양이 함정입니다. TCanvas.Rectangle은 (Left, Top, Right, Bottom)을 받고, 수년간의 VCL 칠하기 코드는 손가락에 두 번째 모서리를 넘기게 훈련시킵니다. HotPDF에 Rectangle(72, 202, 232, 292)를 넘기면 160 × 90 대신 232포인트 폭에 292 높이 상자가 나옵니다. 손에 쥔 숫자가 실제로 두 모서리라면 모서리 좌표를 받아 크기를 계산해 주는 MFRectangle(X, Y, X1, Y1)을 호출하세요. Rectangle은 크기로 사고하는 코드에 아껴 두고요

PDF 파일에는 원 연산자가 없습니다. Circle은 익숙한 0.5523 제어점 인자로 4개의 3차 베지어 구간을 씁니다. 페이지 콘텐츠 스트림을 압축 해제하면 볼 수 있는데, m 하나 뒤에 c 연산자 네 개가 옵니다. Ellipse(X, Y, Width, Height)는 좌상단이 (X, Y)인 경계 상자 안에서 같은 일을 합니다

HotPDF는 현재 페이지를 위한 표준 TCanvas인 THotPDF.Canvas도 노출하고, Graphic 데모의 CanvasDraw 프로그램은 평범한 Canvas.Rectangle, Ellipse, RoundRect 호출로 Ledger 페이지를 채웁니다. 기존 VCL 칠하기 코드를 포팅할 때는 그 경로가 맞습니다. 새 드로잉 코드라면 CurrentPage 쪽이 낫습니다. 각 호출이 포인트 단위의 PDF 연산자 하나에 대응하고 출력이 숫자 단위까지 예측 가능하기 때문입니다

네 번의 커밋으로 만든 콜아웃 상자

콜아웃은 위의 모든 것을 한 번에 씁니다. 채우고 외곽선 칠한 패널, 포인터 선, 앵커 점, 같은 좌상단 좌표로 패널 안에 놓인 두 줄의 텍스트. FillAndStroke(B 연산자)는 하나의 경로 속과 테두리를 한 번의 호출로 칠하므로 사각형이 두 번이 아니라 한 번 만들어집니다

HotPDF 원시 도형으로 조립한 콜아웃 상자의 해부: FillAndStroke로 커밋한 사각형, MoveTo와 LineTo의 스트로크 포인터 선, 채운 앵커 점, 그리고 도형들과 같은 좌상단 격자를 공유하는 TextOut 레이블 띠
네 번의 커밋이 콜아웃을 만듭니다. FillAndStroke가 패널과 테두리를 함께 칠하고, 포인터와 점은 스트로크·채우기 상태를 재활용하며, 모든 레이블 오프셋은 상자 모서리 (90, 122)에 대한 평범한 산수입니다
procedure DrawCallout(Page: THPDFPage);
begin
  // 1. 패널: 연한 채우기와 더 진한 테두리, 하나의 경로, 한 번의 커밋
  Page.SetRGBFillColor(RGB(255, 244, 214));
  Page.SetRGBStrokeColor(RGB(180, 130, 40));
  Page.SetLineWidth(1);
  Page.Rectangle(90, 122, 240, 70);      // 좌상단 (90, 122), 240 x 70
  Page.FillAndStroke;

  // 2. 패널 왼쪽 모서리에서 대상까지 내려가는 포인터
  Page.SetLineWidth(1.5);
  Page.MoveTo(90, 177);
  Page.LineTo(66, 246);
  Page.Stroke;

  // 3. 포인터 끝의 앵커 점, 테두리 색으로
  Page.SetRGBFillColor(RGB(180, 130, 40));
  Page.Circle(64, 250, 3);
  Page.Fill;

  // 4. 레이블: 각 Y는 텍스트 상단, 같은 격자에서 잰 값
  Page.SetFont('Arial', [fsBold], 10);
  Page.TextOut(102, 134, 0, 'Check this total');
  Page.SetFont('Arial', [], 9);
  Page.TextOut(102, 152, 0, 'Rounded to two decimals per line,');
  Page.TextOut(102, 164, 0, 'then summed');
end;

// BeginDoc과 EndDoc 사이에서 호출:
//   DrawCallout(Pdf.CurrentPage);

각 블록은 자기 도형에 필요한 채우기 색, 선 색, 두께를 직전 블록이 같은 값을 남겨 뒀더라도 바로 앞에서 새로 설정합니다. 파일에 몇 개의 중복 연산자가 더 들어가는 값어치로, 어떤 블록이든 앞서 실행된 것이 남긴 엉뚱한 빨간 채우기를 물려받지 않고 옮겨지고, 지워지고, 자기 프로시저가 될 수 있습니다. 보고서가 바뀌듯 드로잉 코드도 바뀌니 이 트레이드는 매번 받을 만합니다

색에도 같은 규율이 필요합니다. Random($FFFFFF)로 채우기를 심으면 데모 페이지는 생기가 돌지만 생성된 보고서는 실행 사이 비교가 불가능해집니다. 같은 데이터의 두 빌드가 다른 바이트를 내기 때문입니다. CanvasDraw 데모가 여덟 항목 CanvasColors 테이블로 하듯 색을 데이터나 고정 팔레트 배열에서 끌어 오면 같은 입력이 같은 파일을 냅니다

대시, 곡선, 투명도

SetDash는 선 두께 같은 페이지 상태입니다. SetDash([3, 3], 0)은 이후 모든 스트로크를 3포인트 켜고 3포인트 끄고로 만들며, 두 번째 인수는 패턴이 시작하는 위치를 밀고, NoDash는 솔리드 펜으로 되돌립니다. 대시 배열은 array of Byte라 길이는 1부터 255까지의 정수 포인트이고 주어진 그대로 콘텐츠 스트림에 기록됩니다. 한 그룹의 그리드선에 건 대시는 그 뒤의 모든 규칙선에 계속 붙어 있으므로, 대시 구간을 GStateSave와 GStateRestore로 감싸세요. 이들은 q와 Q를 내보내고, 저장된 상태를 복원하면 이전 대시와 두께와 색이 한 스텝에 돌아옵니다

곡선은 같은 경로를 늘립니다. CurveToC(X1, Y1, X2, Y2, X3, Y3)는 현재 점에서 (X3, Y3)까지 제어점 두 개의 3차 베지어를 더하고, CurveToV와 CurveToY는 첫 또는 둘째 제어점이 끝점과 겹치는 사례를 맡습니다. 하나의 경로는 Stroke나 Fill 하나 전까지 LineTo와 CurveToC 구간을 섞을 수 있는데, 둥근 모서리와 부드럽게 편 차트 선이 그려지는 방식입니다

투명도는 이름 있는 그래픽 상태에 삽니다. PDF는 알파를 색이 아니라 ExtGState 딕셔너리에 저장하기 때문입니다. THotPDF.RegisterExtGState(FillAlpha, StrokeAlpha)는 0부터 1까지의 값을 받아 이름을 반환하고, CurrentPage.SetGraphicsState(Name)은 이후에 칠해지는 모든 것에 적용합니다. 불투명도마다 문서에 한 번 등록하고 이름을 재활용하세요. 몇 개의 도형이 쓰든 파일은 불투명도마다 딕셔너리 하나만 나릅니다. 알파는 PDF 1.4가 필요하고 HotPDF는 필요할 때 출력 버전을 1.4로 올립니다. StrictVersionLock이 설정돼 있으면 버전은 그대로 두고 빈 이름을 반환합니다. PDFACompliance가 PDF/A-1 프로파일을 고르면 1 미만의 알파는 예외를 던지는데, ISO 19005-1 §6.4가 그곳에서 투명도를 금하기 때문입니다. PDF/A-2와 PDF/A-3 프로파일은 받아들입니다

var
  Half: AnsiString;
begin
  // 문서에 한 번 등록하고 반환된 이름을 어떤 페이지에서든 재활용
  Half := Pdf.RegisterExtGState(0.5, 1.0);   // 50% 채우기, 불투명 스트로크

  Pdf.CurrentPage.GStateSave;                // q
  Pdf.CurrentPage.SetDash([3, 3], 0);
  Pdf.CurrentPage.SetLineWidth(0.5);
  Pdf.CurrentPage.SetRGBStrokeColor(clGray);
  Pdf.CurrentPage.MoveTo(72, 400);
  Pdf.CurrentPage.LineTo(540, 400);
  Pdf.CurrentPage.Stroke;

  Pdf.CurrentPage.SetGraphicsState(Half);
  Pdf.CurrentPage.SetRGBFillColor(clYellow);
  Pdf.CurrentPage.Rectangle(72, 380, 200, 16);  // 텍스트 행 위의 하이라이트
  Pdf.CurrentPage.Fill;
  Pdf.CurrentPage.GStateRestore;             // Q: 솔리드 펜, 불투명도 복원
end;

벡터 호출은 프로그램이 계산한 기하에 맞습니다. 표 규칙선, 차트 축과 막대, 콜아웃, 십여 개 경로로 만든 로고가 그렇습니다. 사진이나 스크린샷은 AddImage와 ShowImage 자리입니다. 픽셀을 경로로 트레이싱하면 파일만 커집니다. 출력을 검사할 때는 두꺼운 스트로크의 모서리와 대시 선 위에서 1600% 확대를 해 보세요. 닫지 않은 경로의 홈과 구간을 새 나온 대시는 그 배율에서는 선명한데 100%에서는 놓치기 쉽습니다

이 글의 모든 호출은 HotPDF Delphi Component의 페이지 객체 위에서 살아갑니다. Delphi와 C++Builder VCL 프로젝트에서 돌고 결과를 순수한 PDF 경로 연산자로 씁니다