네이티브 Delphi/C++Builder PDF 컴포넌트인 HotPDF는 Windows EMF와 WMF 메타파일을 비트맵으로 평면화하는 대신 각 GDI 레코드를 PDF 오퍼레이터로 직접 해석해 임포트한다: 그라디언트 채우기는 PDF 축 방향 셰이딩 패턴이 되고, 해치 브러시는 PDF 타일링 패턴이 되며, 중앙화된 경로 상태 게이트가 잘못된 형식의 레코드가 출력물을 손상시키는 것을 막는다. TChart나 GDI+ 서피스, 또는 단순한 TCanvas가 향상된 메타파일로 내보낼 수 있는 모든 차트가 이 경로의 대상이 되며, 그 차이는 누군가 페이지를 확대하거나 고해상도 프린터로 인쇄하는 순간 바로 드러난다
대부분의 Delphi 개발자가 기본으로 택하는 대안은 메타파일을 페이지에 넣기 전에 비트맵으로 래스터화하는 것인데, 그 비용은 나중에야 드러난다: 화면에서는 선명했던 막대 차트가 PDF를 600 DPI로 인쇄하거나 회의실 화면에 투사하는 순간 눈에 띄게 블록처럼 뭉개지고, 채우기 스타일이 그대로 전달되지 않으면 해치로 채워진 CAD 영역은 단조로운 회색 사각형 하나로 뭉개진다. 메타파일을 그림이 아니라 프로그램으로 읽는 것이 이 두 문제를 모두 피하는 방법이지만, 올바르게 구현하기는 더 어려운 길이며, 그렇기에 보고서가 출고되기 전에 아래의 함정들을 알아둘 가치가 있다
왜 메타파일을 평면화 대신 해석하는가?
HotPDF가 EMF와 WMF 임포트에서 벡터 경로를 고수하는 이유는 Windows 메타파일이 그림이 아니라 기록된 GDI 그리기 호출의 시퀀스이며, 그 호출들을 PDF 경로, 텍스트, 셰이딩 오퍼레이터로 재생하는 것이 결과물을 페이지의 나머지 부분과 동일하게 확대·축소되도록 만드는 방법이기 때문이다. THPDFPage.ShowMetafile과 그 짝인 ShowMetafileEx는 애플리케이션이 호출하는 진입점이며, 둘 다 메타파일을 THPDFWmf 클래스에 넘기는데, 이 클래스가 모든 GDI 레코드를 순회하며 변환한다. 이 구분은 절대적이지 않으며, HotPDF도 그런 척하지 않는다: 예를 들어 StretchDIBits 비트맵 블릿처럼 메타파일 레코드가 진짜 래스터 데이터인 경우에는, 사진을 표현할 수 없는 경로 오퍼레이터로 억지로 밀어넣지 않고 페이지의 다른 그림들과 동일한 AddImage/ShowImage 호출 쌍을 통해 실제 PDF Image XObject로 임베드한다. 선, 채우기, 텍스트는 벡터로 남고, 원본에서 이미 픽셀이었던 것은 출력물에서도 픽셀로 남는다. 가장 단순한 호출은 로드된 메타파일 외에 아무것도 필요하지 않다:
var
Pdf: THotPDF;
Chart: TMetafile;
begin
Pdf := THotPDF.Create(nil);
Chart := TMetafile.Create;
try
Chart.LoadFromFile('quarterly-revenue.emf'); // exported from TChart or GDI+
Pdf.FileName := 'quarterly-report.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.ShowMetafile(Chart);
Pdf.EndDoc;
finally
Chart.Free;
Pdf.Free;
end;
end;
인터프리터는 GDI 좌표를 어떻게 PDF 페이지 공간으로 바꾸는가?
HotPDF는 GDI를 다시 구현하는 대신 메타파일 자신의 레코드 스트림을 한 번 훑는 것으로 이 문제에 답한다. THPDFWmf.Analyse는 Win32 GetEnhMetaFileHeader 호출로 메타파일 헤더를 읽고 내부 그리기 상태를 초기화한 뒤, 메타파일 뷰어가 사용하는 것과 동일한 열거 API인 EnumEnhMetafile을 호출한다. 그 결과 모든 EMR_* 레코드는 원래 기록된 순서 그대로 THPDFWmf.ExecuteRecord에 도달한다. GDI는 메타파일 자체의 매핑 모드가 선택한 디바이스 또는 논리 단위로 좌표를 위에서 아래로 표현하는 반면, PDF 페이지는 HotPDF의 경로와 채우기를 위한 캔버스 그리기 모델에서 다루는 사용자 공간 포인트 단위로 아래에서 위로 표현된다. 각 레코드 핸들러는 ScaleX와 ScaleY를 통해 이 불일치를 해소하는데, 이 함수들은 ProjectX와 ProjectY를 호출해 비등방성(anisotropic) 및 등방성(isotropic) 매핑 모드에 대한 GDI 자체의 윈도우-뷰포트 변환 공식을 그대로 재현한다. 그 덕분에 논리 단위로 폭 5로 기록된 도형은 소스 애플리케이션이 어떤 윈도우/뷰포트 범위를 설정했든 상관없이 PDF 포인트 단위에서 정확한 폭으로 배치된다
GDI 그라디언트 채우기는 어떻게 PDF 셰이딩 패턴이 되는가?
GDI가 두 사각형 모드 중 하나로 기록한 경우, EMR_GRADIENTFILL 레코드는 진짜 PDF Type 2 축 방향 셰이딩 패턴(ISO 32000-1 §8.7.4.5)이 된다. THPDFWmf.VEMRGradientFill은 MS-EMF §2.3.1.6 구조를 따라 레코드 자체의 레이아웃을 원시 바이트 버퍼에서 직접 읽는다: 16비트 RGBA 모서리로 이루어진 정점 배열 뒤에, 각각 그 정점 중 두 개를 참조하는 사각형 목록이 이어지는 구조다. GRADIENT_FILL_RECT_H의 경우 색상은 사각형의 수평 중간선을 따라 좌에서 우로 흐르고, GRADIENT_FILL_RECT_V의 경우 수직 중간선을 따라 위에서 아래로 흐른다. 어느 쪽이든 두 모서리 색상과 투영된 사각형 좌표는 곧바로 THotPDF.RegisterAxialGradient로 전달되어 패턴 이름을 반환받고, 페이지는 평범한 SetRGBFillColor 호출 대신 그 패턴(SetFillPattern)을 통해 사각형을 그리고 채운다. 그래서 스프레드시트 스타일의 밴딩 헤더나 차트의 그라디언트 플롯 영역은 평균 색상 하나로 뭉개지지 않고 그라디언트를 그대로 유지한다
구로 삼각형(Gouraud triangle) 모드는 솔직히 인정하는 공백 지점이다. 레코드의 ulMode 필드가 GRADIENT_FILL_TRIANGLE을 나타내면 VEMRGradientFill은 이를 인식하고 삼각형 모드가 아직 구현되지 않았다는 로그를 남긴 뒤, 두 색 근사치로 어림짐작하는 대신 해당 사각형을 건너뛴다. 임의의 삼각형 메시 전체에 걸친 정점별·픽셀별 보간은 두 스톱짜리 축 방향 또는 방사형 셰이딩으로 환원되지 않으며, 이를 제대로 표현하려면 PDF Type 4 또는 Type 5 메시 셰이딩을 출력해야 한다. 이는 HotPDF의 페이지 렌더러가 PDF를 다시 읽어들일 때도 마찬가지로 그리지 않고 남겨두는 셰이딩 계열이다. 서로 무관한 두 코드 경로가 같은 경계에 다다르는 셈이다: 메시 셰이딩은 쓰기 쪽과 읽기 쪽 모두에서 공백이며, 부드러운 방사형 광채를 위해 구로 삼각형을 사용하는 소스 다이어그램은 렌더링된 근사치가 아니라 마지막으로 사용된 단색 브러시로 대체된다
해치 브러시는 평면화된 회색이 아니라 타일링 패턴이 된다
GDI 해치 브러시가 PDF에서도 질감을 유지하는 이유는 THPDFWmf.SetBrushColor가 단색 채우기로 폴백하기 전에 항상 CurrentBrush.lbStyle이 BS_HATCHED인지 먼저 확인하고, 그 경우를 SetHatchBrushPattern으로 라우팅하기 때문이다. 이 메서드는 GDI 해치 스타일에 따라 선택된 8x8 단위의 PDF 콘텐츠 스트림을 m, l, S 같은 스트로크 선 오퍼레이터로 작성한다: HS_HORIZONTAL과 HS_VERTICAL은 수평 또는 수직 스트로크 하나, HS_FDIAGONAL과 HS_BDIAGONAL은 세 개의 평행 대각선, HS_CROSS와 HS_DIAGCROSS는 수평+수직 또는 양쪽 대각선의 조합이다. THotPDF.RegisterTilingPattern은 이 콘텐츠 스트림을 8단위 XStep과 YStep을 가진 컬러 타일링 패턴(PaintType 1, ISO 32000-1 §8.7.3.1)으로 등록하고, 페이지는 축 방향 셰이딩과 동일한 방식으로 SetFillPattern을 통해 채운다. 재질을 구분하기 위해 해치 채우기에 의존하는 CAD 평면도나 공학 도면은 모든 영역이 동일한 회색으로 뭉개지는 대신 그 시각적 언어를 PDF에서도 그대로 유지한다
모든 브러시가 이런 대우를 받는 것은 아니며, CAD 임포트를 출고하기 전에 이 공백을 알아둘 가치가 있다. GDI의 여섯 가지 기본 해치 스타일이 아닌 커스텀 비트맵 이미지 패턴 브러시용 레코드인 EMR_CREATEDIBPATTERNBRUSHPT는 이후 SELECTOBJECT와 DELETEOBJECT 레코드가 일관성을 유지하도록 핸들만 등록할 뿐이다. HotPDF는 아직 임의의 타일 이미지를 위한 PDF Pattern 리소스 파이프라인을 노출하지 않으므로, 그 브러시를 선택하면 원본 텍스처 대신 단색 폴백으로 떨어진다. 원본이 명백히 반복되는 이미지 텍스처를 사용했는데 채우기가 평평하게 렌더링된다면, 소스 브러시는 표준 해치가 아니라 거의 확실히 커스텀 DIB 패턴이며, 이것이 가장 먼저 수동으로 확인해 볼 가치가 있는 경우다. 이런 도면을 위한 임포트 설정도 동일한 옵션 객체를 거친다:
var
Pdf: THotPDF;
Drawing: TMetafile;
Options: THPDFEmfOptions;
begin
Pdf := THotPDF.Create(nil);
Drawing := TMetafile.Create;
Options := THPDFEmfOptions.Create;
try
Drawing.LoadFromFile('floor-plan.emf');
Options.Assign(Pdf.EmfOptions); // start from the document-wide defaults
Options.Redraw := False; // interpret the original EMF bytes, no GDI re-record pass
Options.ShowNullBrush := True; // keep explicitly unfilled CAD regions visible
Options.UseFrame := True; // clip output to the frame the EMF header declares
Pdf.FileName := 'floor-plan.pdf';
Pdf.BeginDoc;
Pdf.CurrentPage.ShowMetafileEx(Drawing, Options);
Pdf.EndDoc;
finally
Options.Free;
Drawing.Free;
Pdf.Free;
end;
end;
잘못된 형식의 메타파일이 페이지를 손상시키지 못하도록 막는 것은 무엇인가?
HotPDF의 답은 약 80개에 이르는 각 레코드 핸들러마다 방어 코드를 반복하는 대신 ExecuteRecord 최상단에 두는 단일 게이트다. EMR_BEGINPATH로 열리고 EMR_ENDPATH 또는 EMR_ABORTPATH로 닫히는 GDI 경로 괄호는 FPathContinue 필드가 뒷받침하는 비공개 PathContinue 속성으로 추적된다. 그 괄호가 열려 있는 동안 ExecuteRecord는 이동, 선, 폴리라인, 폴리곤, 폴리베지어, 폴리드로우 계열의 경로 구성 레코드와 CLOSEFIGURE, 그리고 SETWORLDTRANSFORM, SAVEDC, RESTOREDC 같은 소수의 변환·DC 상태 레코드만 통과시킨다. 괄호가 열려 있는 동안 ExecuteRecord에 도달하는 그 외의 모든 레코드 타입, 예를 들어 뜬금없는 EXTTEXTOUT이나 비트맵 블릿은 도착하는 즉시 단일 Exit로 중앙에서 폐기된다
이 게이트가 존재하는 이유는, 손으로 작성했거나 도구가 생성했거나 단순히 손상된 메타파일의 경로 괄호가 정상적인 파일이 시작·종료 레코드 사이에 넣었을 법한 내용만 담고 있다고 보장할 수 없기 때문이다. 게이트가 없다면 EMR_BEGINPATH와 EMR_ENDPATH 사이에 텍스트 출력 레코드가 끼어드는 경우, 구성 중인 경로 지오메트리를 오염시키거나 순수한 경로 구성이어야 할 시퀀스 중간에 PDF 텍스트 표시 오퍼레이터를 내보내게 되는데, 두 실패 모드 모두 일반적인 테스트 스위트가 우연히 다루는 것이 아니라 서드파티 도구가 만든 잘못된 형식의 입력 하나에서 표면화되는 종류다. 이 검사를 ExecuteRecord에 집중시킨 덕분에 개별 VEMR* 핸들러들은 각자 잘못된 시점에 호출되는 상황을 방어할 필요가 없다. 게이트가 디스패치 전에 한 번 결정하는 것이지, 디스패치 후 여든 번 결정하는 것이 아니다
한 페이지에 벡터 차트와 텍스트, 이미지를 함께 배치하기
보고서 페이지에 차트만 있는 경우는 드물며, ShowMetafile은 다른 어떤 그리기 호출과도 마찬가지로 HotPDF의 다른 페이지 오퍼레이터들과 자연스럽게 조합된다. TextOut으로 그린 제목, EMF로 임포트한 해치 채우기 막대 차트, ShowImage로 배치한 로고가 모두 각자의 원본 품질을 유지한 채 같은 페이지, 같은 콘텐츠 스트림에 놓일 수 있으며, 이 조합 방식은 보고서에서 텍스트, 폰트, 이미지를 배치하는 HotPDF 가이드에서 다룬다:
Pdf.CurrentPage.SetFont('Arial', [fsBold], 14);
Pdf.CurrentPage.TextOut(50, 760, 0, 'Q2 Regional Sales');
Pdf.CurrentPage.ShowMetafile(RegionChart); // hatch-filled bars, still vector
Pdf.CurrentPage.ShowImage(LogoIndex, 450, 760, 90, 30, 0);
여기서 설명한 EMF/WMF 인터프리터, 그라디언트 채우기를 위해 등록되는 축 방향 셰이딩 패턴, 해치 브러시를 위한 타일링 패턴 매핑은 모두 Delphi와 C++Builder용 HotPDF 컴포넌트 표준판에 포함되어 있으며, 이 기능 전체에 외부 DLL 의존성이 없는 네이티브 VCL 라이브러리다