기술 문서

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

HotPDF는 현재 페이지에 경로를 구축한 다음 칠하도록 요청하여 벡터 그래픽을 그립니다. 중간에 비트맵 단계는 없습니다. MoveToLineTo로 그린 선은 콘텐츠 스트림에서 PDF 경로 연산자로 끝납니다. 따라서 이는 진정한 벡터로 유지됩니다: 50% 축소에서도 선명하고 1600% 확대에서도 선명하며, 래스터화된 버전이 소모하는 크기의 일부에 불과합니다. 다이어그램, 표의 선, 차트 축 및 폼 장식의 경우 이것이 정확히 여러분이 원하는 것이며, 그 이면의 API는 한 번에 배울 수 있을 만큼 작습니다

전체 그리기 표면은 THotPDF.CurrentPage에 존재합니다. BeginDocEndDoc 사이에 해당 페이지 객체의 색상과 선 너비를 설정하고 기하학적 형태를 놓은 다음 칠하기 연산자를 호출하여 커밋(commit)합니다. 가장 많이 사용할 네 가지 원시 요소(primitives)는 임의의 경로를 위한 MoveToLineTo, 상자를 위한 Rectangle, 디스크를 위한 Circle, 그리고 두 가지 칠하기 연산자인 StrokeFill입니다

좌표계는 왼쪽 하단입니다

이것은 VCL에서 온 모든 사람을 걸려 넘어지게 하는 한 가지입니다. 컨트롤을 칠하는 데 사용하는 TCanvas는 Y가 아래로 증가하는 왼쪽 상단에 원점을 둡니다. PDF는 정반대입니다. HotPDF는 페이지의 왼쪽 하단 모서리부터 포인트(1/72 인치) 단위로 측정하며 Y는 위로 이동할수록 증가합니다. Y := 720의 위치는 792포인트 높이인 US Letter 페이지 상단 근처에 있고, Y := 50은 하단 근처에 위치합니다. 여러분의 첫 그림이 세로로 대칭되어 나온다면 그 이유는 다음과 같습니다: 화면 그래픽에서 이식된 코드는 잘못된 방향을 가정하고 하단 가장자리를 벗어나 실행됩니다

동일한 규칙이 TextOut을 통제하므로 일단 여러분이 이를 내재화하고 나면 텍스트와 도형은 하나의 멘탈 모델을 공유하게 됩니다. 상단이 아니라 각 요소의 하단이 위치할 곳을 결정하여 레이아웃을 계획하면 나머지는 자연스럽게 따라옵니다

경로: MoveTo, LineTo, Stroke

테두리가 그려진 경로(stroked path)는 펜을 들고, 놓고, 끈 것입니다. MoveTo는 펜을 들어 아무것도 표시하지 않고 시작점을 설정합니다. 각각의 LineTo는 현재 경로를 새로운 점으로 연장합니다. 현재 테두리 색상과 선 너비를 사용하여 누적된 경로를 그리는 Stroke를 호출할 때까지 페이지에 아무것도 나타나지 않습니다. 그런 다음 다음 MoveTo가 새로 시작되도록 경로를 지웁니다

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'DrawPaths.pdf';
    Pdf.BeginDoc;

    // Line width is in points and applies until you change it.
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.SetRGBStrokeColor(clBlack);

    // A horizontal rule near the top of the page (Y measured from bottom).
    Pdf.CurrentPage.MoveTo(72, 720);
    Pdf.CurrentPage.LineTo(523, 720);
    Pdf.CurrentPage.Stroke;          // commit the path; nothing drew before this

    // A thicker connected polyline: three segments in one path.
    Pdf.CurrentPage.SetLineWidth(3);
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(30, 90, 200));
    Pdf.CurrentPage.MoveTo(72, 640);
    Pdf.CurrentPage.LineTo(172, 690);
    Pdf.CurrentPage.LineTo(272, 620);
    Pdf.CurrentPage.LineTo(372, 680);
    Pdf.CurrentPage.Stroke;

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

두 가지 세부 사항이 실제 디버깅 시간을 절약합니다. 선 너비는 인수가 아니라 상태입니다: SetLineWidth가 한 번 설정하면 변경하기 전까지 모든 후속 Stroke는 그 값을 사용하며, 위의 폴리라인(polyline)이 일반 선보다 두꺼운 이유가 바로 이 때문입니다. 그리고 각 Stroke 후에 경로가 재설정되므로 Stroke를 잊어버리면 공들여 배치한 기하학적 형태가 렌더링되지 않음을 의미합니다. 출력에서 도형이 누락된 경우 칠하기(painting) 호출을 가장 먼저 살펴보아야 합니다

좌표는 포인트이며 포인트는 소수(fractional)를 가질 수 있습니다. MoveToLineToSingle 값을 허용하므로 0.5 포인트의 가는 선이나 72.25 위치는 합법적이고 의미가 있으며 가장 가까운 정수 단위로 반올림되지 않습니다. 그 정밀도는 정반대의 두 방향에서 중요합니다. 약 0.5 미만의 선 너비는 화면에서는 사라지고 인쇄할 때는 다시 나타나는 디바이스 종속적인 가능한 한 가장 얇은 선으로 렌더링될 수 있으므로 보이는 선은 기본값이 아닌 의도적으로 설정한 너비를 원합니다. 반면에 표의 선과 눈금선(gridline)을 정수 좌표에 맞추면 인접한 선이 다르게 반올림되어 조밀한 눈금이 약간 고르지 않게 보이는 것을 방지할 수 있습니다. 눈금 간격을 포인트 단위로 미리 결정하면 나머지 레이아웃도 그것을 상속합니다

채워진 도형과 색상

닫힌 원시 요소는 윤곽선 대신 채워질 수 있습니다. Rectangle은 위치와 크기를, Circle은 중심과 반지름을 취하며 어느 쪽이든 현재 채우기 색상으로 내부를 칠하는 Fill 또는 윤곽선만 그리는 Stroke로 커밋됩니다. 채우기 색상과 테두리 색상은 별개의 상태 조각이며 각각 단일 TColor를 사용하는 SetRGBFillColorSetRGBStrokeColor로 설정됩니다. 즉, 델파이의 색상 상수와 RGB 도우미(helper)를 직접 재사용할 수 있습니다

// Rectangle(X, Y, Width, Height): X and Y are the lower-left corner.
Pdf.CurrentPage.SetRGBFillColor(RGB(220, 60, 60));
Pdf.CurrentPage.Rectangle(72, 500, 160, 90);
Pdf.CurrentPage.Fill;

// Circle(X, Y, Radius): X and Y are the center.
Pdf.CurrentPage.SetRGBFillColor(clNavy);
Pdf.CurrentPage.Circle(420, 545, 45);
Pdf.CurrentPage.Fill;

// Outline only: set a stroke color and a width, then Stroke.
Pdf.CurrentPage.SetLineWidth(2);
Pdf.CurrentPage.SetRGBStrokeColor(clBlack);
Pdf.CurrentPage.Rectangle(72, 400, 160, 60);
Pdf.CurrentPage.Stroke;

Rectangle 인수의 모양을 주의해서 보세요. 이것은 위치와 크기 즉, X, Y, Width, Height이며 대각선 두 모서리가 아닙니다. 델파이 개발자들이 아는 TCanvas.Rectangle(Left, Top, Right, Bottom)을 취하므로 근육 기억(muscle memory)이 너비와 높이를 예상하는 곳에 두 번째 모서리를 HotPDF로 전달하게 되고, 그러면 상자는 잘못된 크기로 나옵니다. 페이지 원점과 일관되게 (X, Y) 쌍은 왼쪽 하단 모서리입니다. 원의 경우 (X, Y)는 중심이고 세 번째 인수는 포인트 단위의 반지름입니다

원래 예제에서 잘못된 색상 선택 하나

이 예제의 이전 버전은 모든 도형에 Random($FFFFFF)를 사용하여 색상을 시드했습니다. 이것은 활기차 보일 수 있으나 생성된 문서에는 잘못된 본능입니다. 코드로 구축하는 PDF는 일반적으로 런타임마다 테스트하기를 원하는 것이며, 무작위 채우기 색상은 실행마다 출력 비교를 불가능하게 만듭니다: 알려진 양호한 파일과 비교하는 바이트 대 바이트 diff는 진짜 이유 없이 매번 실패합니다. 명시적인 색상을 선택하세요. 일련의 도형에서 변화를 주고 싶을 때는 동일한 입력이 항상 동일한 파일을 생성하도록 데이터나 고정된 팔레트 배열로 색상을 구동(drive)하세요. 결과물이 릴리스 파이프라인을 통과할 때는 참신함보다 결정론(determinism)이 훨씬 가치 있습니다

원시 요소를 함께 조합하기: 콜아웃(callout) 상자

각 원시 요소는 그 자체로 단순합니다; 그 진가는 소수의 요소가 모여 보고서에 실제로 필요한 것으로 조합될 때 나타납니다. 그림을 가리키며 설명하는 주석 상자인 콜아웃(callout)은 테두리가 있는 채워진 사각형, 테두리가 그려진 포인터 선, 포인터를 고정하는 점(dot), 그리고 도형이 사용하는 것과 동일한 왼쪽 하단 좌표계를 사용하여 상자 안에 레이아웃된 텍스트 등 지금까지 다룬 모든 것을 사용합니다. FillAndStroke는 여기서 진가를 발휘하여, 사각형을 두 번 구축하는 대신 한 번의 커밋으로 하나의 경로에 대한 내부와 윤곽선을 모두 칠합니다

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'Callout.pdf';
    Pdf.BeginDoc;

    // 1. The box: pale fill plus a visible border, one path, one commit.
    //    Rectangle is lower-left corner plus size, Y measured from the bottom
    Pdf.CurrentPage.SetRGBFillColor(RGB(255, 244, 214));   // pale amber panel
    Pdf.CurrentPage.SetRGBStrokeColor(RGB(180, 130, 40));  // darker rim
    Pdf.CurrentPage.SetLineWidth(1);
    Pdf.CurrentPage.Rectangle(90, 600, 240, 70);
    Pdf.CurrentPage.FillAndStroke;

    // 2. The pointer: one stroked segment from the box edge down
    //    toward the thing being annotated
    Pdf.CurrentPage.SetLineWidth(1.5);
    Pdf.CurrentPage.MoveTo(90, 615);        // left edge of the box
    Pdf.CurrentPage.LineTo(66, 546);
    Pdf.CurrentPage.Stroke;

    // 3. A filled dot anchors the pointer at its target
    Pdf.CurrentPage.SetRGBFillColor(RGB(180, 130, 40));
    Pdf.CurrentPage.Circle(64, 542, 3);
    Pdf.CurrentPage.Fill;

    // 4. The label, positioned relative to the box's lower-left corner.
    //    Text and shapes share one coordinate system, so the offsets
    //    are plain arithmetic against (90, 600)
    Pdf.CurrentPage.SetFont('Arial', [fsBold], 10);
    Pdf.CurrentPage.TextOut(102, 645, 0, 'Check this total');
    Pdf.CurrentPage.SetFont('Arial', [], 9);
    Pdf.CurrentPage.TextOut(102, 628, 0, 'The rounding rule changed in the');
    Pdf.CurrentPage.TextOut(102, 616, 0, 'June release; verify against v2.1');

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

합성(composite)에 필요한 상태 관리가 얼마나 적은지 확인해 보세요. 채우기 색상, 테두리 색상 및 선 너비는 각각 해당 상태를 사용하는 도형 직전에 설정되므로, 그림의 모든 블록은 독립적인 단위(self-contained unit)로 읽히며 숨겨진 상태를 끌고 다니지 않고도 재정렬되거나 도우미 메서드로 추출될 수 있습니다. 고정점(anchor point)과 문자열을 매개변수로 받는 프로시저에 이를 래핑하면 40줄의 코드로 재사용 가능한 다이어그램 주석을 얻게 됩니다

벡터 드로잉이 유용한 경우와 그렇지 않은 경우

차트의 눈금선(gridline)과 막대, 송장 표의 줄, 다이어그램의 콜아웃 상자, 몇 개의 경로로 표현된 로고 마크와 같이 기하학적 형태가 생성될 때 이 경로와 도형 호출을 선택하세요. 사각형은 수천 개의 픽셀이 아니라 소수의 숫자로 표현되기 때문에 이 모든 것은 흐림(blur) 없이 크기가 조정되고 파일 크기를 거의 증가시키지 않습니다. 반대의 경우도 마찬가지입니다. 실제로 가지고 있는 것이 사진이나 스크린샷인 경우 비트맵을 벡터 호출로 추적하는 것은 아무것도 얻을 수 없으므로 AddImageShowImage를 사용하여 이미지로 그리세요. 위의 직선 세그먼트, 사각형 및 원은 실제 보고서 작업의 대부분을 차지하며, 개발자가 다음으로 질문하는 세 가지 개선 사항인 곡선, 대시 패턴 및 투명도는 동일한 페이지 객체에 존재합니다

곡선, 대시, 그리고 투명도 요약

자유 형식 곡선은 여러분이 이미 가지고 있는 동일한 경로 기계(machinery)를 확장합니다. CurveToC(X1, Y1, X2, Y2, X3, Y3)는 현재 점에서 (X3, Y3)로 두 제어점을 향해 구부러지는 3차 베지어(cubic Bezier) 세그먼트를 추가하며, 단축(shorthand) 변형인 CurveToVCurveToY는 한 제어점이 끝점과 일치하는 경우를 다룹니다. 경로는 단일 Stroke 또는 Fill로 커밋하기 전에 LineToCurveToC 세그먼트를 자유롭게 혼합할 수 있으며, 둥근 모서리와 부드러운 차트 선은 이러한 방식으로 만들어집니다

대시(dashed)로 그려진 선(stroke)은 선 너비와 똑같이 상태입니다. SetDash([3, 3], 0)은 모든 후속 그리기를 3포인트 on, 3포인트 off 패턴으로 전환합니다. 배열은 포인트 단위의 on/off 실행 길이를 나타내고 두 번째 인수는 주기가 시작되는 단계를 지정합니다; NoDash는 펜을 실선으로 되돌립니다. 이것을 설정하고 필요한 눈금선을 그린 후, 다음 실선 그리기 전에 재설정하세요. 그렇지 않으면 대시가 그 다음에 오는 모든 것을 은밀하게 감염시킵니다

투명도는 색상 인수(color argument) 대신 명명된 그래픽 상태(named graphics state)를 통해 실행됩니다. PDF에서 알파는 그래픽 상태 딕셔너리의 속성이기 때문입니다. 0과 1 사이의 채우기 알파(fill alpha)와 테두리 알파(stroke alpha)를 전달하여 RegisterExtGState로 문서에 이를 등록한 다음, 그것이 반환하는 이름을 CurrentPage.SetGraphicsState에 적용하세요. 해당 시점부터의 채우기와 그리기는 등록된 불투명도로 칠해집니다. 색상 설정기보다 번거로운 의식이지만 하이라이트 표시줄이 텍스트를 숨기지 않고 그 위에 있어야 할 때 처음으로 사용할 가치가 있습니다

유지할 가치가 있는 나머지 습관은 검증입니다. 생성된 기하학적 구조는 사용자의 기계에서는 통과하지만 고객의 기계에서는 실패할 수 있으며, 이는 혼합된 텍스트의 폰트 대체나 성립하지 않는 페이지 크기 가정이 원인인 경우가 많습니다. 완료된 파일을 몇 가지 줌 레벨에서 열어 모서리가 깨끗한지 확인하고, 모든 도형이 의도한 여백 상자 내에 위치하는지 확인하세요. 결정론적인 색상 방식을 사용하면, 눈으로 직접 확인하는 대신 참조 PDF에 대해 이 검증을 자동화할 수 있습니다

여기에 표시된 MoveTo, LineTo, Stroke, Fill 및 색상 호출은 델파이 및 C++Builder용 HotPDF 컴포넌트의 일부입니다