HotPDF는 시스템에 추가 폰트를 설치하지 않고도 Delphi에서 임베디드 PDF 글꼴을 렌더링합니다: HPDFGlyphRender.pas에 내장된 임베디드 글리프 렌더링 파이프라인은 PDF 자체 내부에 저장된 폰트 프로그램(FontFile2의 TrueType glyf 아웃라인, FontFile3의 CFF Type 2 캐릭터스트링(charstrings), Type 3 글리프 콘텐츠 스트림 등)을 파싱하여 채워진 GDI 벡터 패스로 재현합니다. 본 기사는 HotPDF로 PDF 페이지를 TBitmap으로 렌더링하기 기사의 뒤에 숨겨진 글꼴 정밀도에 관한 심층 탐구입니다: 이전 글이 렌더러 전체를 다루었다면, 이 글은 해당 페이지의 텍스트가 어떻게 정확한 모양을 갖추는지 다룹니다
렌더링된 PDF 출력물에서 네모 상자(두부 현상), 공백 또는 미묘하게 잘못된 문자가 나타나는 것은 거의 항상 렌더러가 파일에 임베디드된 글꼴을 사용하는 대신 운영 체제에 해당 글꼴을 요청했음을 의미합니다. 이러한 불만 사항은 매번 동일한 패턴으로 접수됩니다: 문서를 작성한 시스템에서는 완벽해 보이지만, 고객이 깨끗한 서버나 보안이 강화된 데스크톱에서 문서를 열었을 때 일본어 송장이 온통 네모 상자로 표시되거나, 대체된 유사 글꼴로 인해 줄바꿈 위치가 어긋나는 경우입니다. 해당 글꼴은 그 시스템에 설치된 적이 없으며 오직 PDF 내부안에만 존재하는데, 시스템 글꼴 대체 방식으로만 작동하는 렌더러는 이를 표시하지 못합니다. 서브셋(subset) 글꼴은 상황을 더 악화시킵니다: 서브셋은 해당 파일 전용으로 사설 할당된 문자 코드 아래에 40여 개의 글리프만 포함할 수 있으며, 설치된 글꼴 중에서는 이러한 매핑을 공유하는 글꼴이 없습니다
아웃라인 연산의 차이와 인코딩 레이어
ISO 32000-1 §9.9는 글꼴 기술자(descriptor)에서 임베디드 글꼴 프로그램을 전달하는 세 가지 통로를 정의합니다: FontFile은 원본 Type 1 프로그램을, FontFile2는 TrueType 프로그램을, FontFile3은 가공되지 않은 CFF(Type1C 또는 CIDFontType0C) 또는 OpenType 래퍼를 유지합니다. 네 번째 형태인 ISO 32000-1 §9.6.5의 Type 3 글꼴은 바이너리 데이터를 전혀 포함하지 않으며, 각 글리프는 그 자리에서 실행되는 소형 PDF 콘텐츠 스트림입니다. 이 세 가지 방식은 아웃라인 연산(2차 B-스플라인 대 3차 캐릭터스트링 대 임의의 페이지 연산자)에서 큰 차이가 있으므로, 정밀한 렌더러는 각각에 대한 개별 인터프리터와 아웃라인을 처리하기 전에 문자 코드를 올바른 글리프 인덱스로 변환하는 인코딩 계층을 필요로 합니다
HotPDF는 어떻게 TrueType glyf 아웃라인을 GDI 패스로 변환합니까?
HPDFGlyphRender.pas의 THPDFEmbeddedTTF는 loca 테이블을 읽어 각 글리프 레코드의 위치를 파악하고, glyf 윤곽선(contour)을 점별로 탐색하며 GDI 패스를 생성합니다. 두 가지 TrueType 규칙을 명시적으로 처리해야 합니다. 첫째, 연속된 곡선 외부(off-curve)의 점은 그 중점을 곡선 내부(on-curve)의 점으로 보간(imply)하며, 모든 점이 곡선 외부의 점인 윤곽선은 마지막 점과 첫 번째 점의 중점에서 시작합니다 — 두 규칙 중 하나라도 누락되면 둥근 글리프가 평평해지거나 무너집니다. 둘째, TrueType 곡선은 2차 베지에(Bézier) 곡선인 반면 GDI의 PolyBezierTo는 3차 곡선을 허용하므로, 각 2차 세그먼트는 선분으로 단순화(flattened)되는 대신 정확하게 3차 베지에 곡선으로 차수 승격(degree-elevated)됩니다
// Exact degree elevation: quadratic (P0, Q, P2) -> cubic (P0, C1, C2, P2)
// C1 = P0 + 2/3 (Q - P0), C2 = P2 + 2/3 (Q - P2)
C1.X := P0.X + 2 * (Q.X - P0.X) / 3;
C1.Y := P0.Y + 2 * (Q.Y - P0.Y) / 3;
C2.X := P2.X + 2 * (Q.X - P2.X) / 3;
C2.Y := P2.Y + 2 * (Q.Y - P2.Y) / 3;
// then PolyBezierTo with C1, C2, P2 — geometrically identical curve
차수 승격은 무손실 방식입니다: 3차 곡선은 원래와 동일한 궤적을 그리므로, 렌더링된 아웃라인은 모든 줌 배율에서 규격을 준수하는 뷰어가 동일한 테이블로부터 그린 것과 완벽히 일치합니다. 나머지 작업은 배치입니다. 각 글리프는 폰트 단위(일반적으로 1000 또는 2048 units-per-em 그리드)로 구성되며, 렌더러는 패스가 채워지기 전에 스케일 행렬, 텍스트 행렬 및 현재 변환 행렬(CTM)을 하나의 글리프-디바이스 변환 행렬로 결합합니다. 행렬 곱셈의 순서가 매우 중요합니다: 동일한 세 행렬을 역순으로 결합하면 모든 글리프가 원점 방향으로 수축하여 무너지게 됩니다. 폼이 왜곡되어 렌더링되는 페이지의 실제 버그 원인이 단 한 줄의 행렬 대수식 순서 때문인 경우가 이에 해당합니다
Type 2 캐릭터스트링 인터프리터가 CFF 글꼴을 처리하는 방법
THPDFEmbeddedCFF는 FontFile3 프로그램에 실제 Type 2 캐릭터스트링 인터프리터를 적용합니다: CFF INDEX 구조, Top DICT 및 Private DICT를 파싱한 다음, 각 캐릭터스트링을 실행하고 경로 세그먼트를 GDI에 직접 전달합니다. 순수한 CFF 테이블에 도달하기 위해 OpenType 래퍼(OTTO 컨테이너)를 먼저 제거하고, 가공되지 않은 CIDFontType0C 및 Type1C 스트림은 바로 사용됩니다. 캐릭터스트링은 콤팩트한 스택 지향 언어이며, 인터프리터가 바이트 스트림과 동기화를 유지할지 여부는 세 가지 규칙에 좌우됩니다. 첫째, 선택적 너비 접두사는 첫 번째 스택 클리어 연산자가 하나의 추가 피연산자를 전달할 수 있음을 의미합니다. 둘째, 피연산자가 스택에 여전히 존재할 때 hintmask 연산자는 vstemhm을 수반하며, 건너뛸 마스크 바이트 수는 누적된 스템(stem) 수에 따라 달라집니다 — 이 개수를 한 번이라도 잘못 계산하면 이후의 모든 연산 코드를 잘못 해석하게 됩니다. 셋째, 서브루틴 호출은 조회를 수행하기 전에 서브루틴 개수에 따라 인덱스 바이어스(107, 1131 또는 32768)를 추가하므로, 바이어스를 누락한 호출은 완전히 엉뚱한 서브루틴을 로드하게 됩니다
CID 키 지정 CFF는 일반적인 구현에서 누락되기 쉬운 한 단계의 간접 참조를 추가합니다: 문자는 CID를 선택하지만 캐릭터스트링 인덱스는 GID이며, 폰트의 charset은 GID를 CID로 매핑합니다. 따라서 렌더러는 그리기 전에 역방향 CID-GID 매핑을 구축하고 여러 Private DICT를 제공하는 글꼴에 대해 FDSelect를 사용하여 글리프별 Private DICT를 선택합니다. 간단한 Type 1 글꼴의 일반적인 캐리어인 이름 키 지정 Type1C 프로그램은 CFF 프로그램 자체에 내장된 인코딩 또는 다음에 설명할 PDF 수준의 인코딩 메커니즘을 통해 1바이트 코드를 확인합니다. 한 가지 한계점: 인터프리터는 스트림 동기화를 유지하기 위해 힌트 연산자를 읽어 들이기는 하지만 힌팅(hinting)을 실제로 실행하지는 않으며, 이에 대해서는 마지막 부분에서 설명합니다
Type 3 글꼴의 정의와 드로잉 방법
Type 3 글리프는 아웃라인 구조가 아닙니다 — ISO 32000-1 §9.6.5는 이를 콘텐츠 스트림으로 정의하므로, HotPDF는 그래픽 상태를 푸시하고 폰트 행렬, 폰트 크기 및 텍스트 행렬을 CTM에 결합한 후 폰트 고유의 /Resources 범위 내에서 페이지를 그리는 것과 동일한 연산자 인터프리터를 통해 글리프 프로시저를 실행하여 렌더링합니다. 사양상 두 가지 세부 사항이 정확성에 기여합니다. 첫째, Type 3의 /Widths는 다른 글꼴 형식이 사용하는 1/1000 텍스트 공간이 아닌 글리프 공간으로 표현되므로 /FontMatrix를 거쳐 진행해야 합니다 — 그렇지 않으면 0.01 행렬을 가진 바코드 글꼴의 간격 조정이 크게 왜곡될 수 있습니다. 둘째, d1 연산자로 시작하는 글리프 프로시저는 렌더러가 보장하는 두 가지 사항을 약속합니다: 드로잉은 선언된 경계 상자(bounding box)로 클리핑되며, ISO 32000-1 §9.6.5.2에 따라 글리프는 프로시저 내부의 자체 색상 연산자를 무시하고 호출자의 현재 채우기 색상으로 칠해집니다. 따라서 프로시저 내부의 rg, g, k 및 선 그리기 연산자들은 글리프가 실행되는 동안 억제됩니다. 색상 규칙을 누락하면 페이지에서 파란색으로 찍힌 d1 바코드 글꼴이 검은색으로 표시되며, 클리핑을 누락하면 잘못 구성된 글리프가 셀 외부 영역에 침범해 칠해집니다
문자 코드가 글리프 ID로 변환되는 방법
아웃라인 인터프리터는 전체 작업의 절반에 불과합니다. PDF 텍스트 문자열의 바이트는 글리프 인덱스가 아니라 문자 코드이기 때문이며, ISO 32000-1 사양은 이 매핑에 대해 두 개의 절을 할당하고 있습니다. 단순 글꼴의 경우 §9.6.6은 엄격한 우선순위를 규정합니다: /Differences 배열이 기본 인코딩(WinAnsiEncoding, MacRomanEncoding 또는 StandardEncoding)보다 우선하며, 이는 다시 글꼴 프로그램 자체의 맵보다 우선합니다. HotPDF는 이 관계를 256개 항목의 코드-GID 테이블로 풀고 세 가지 경로를 통해 글리프 이름을 글리프 인덱스로 번역합니다: CFF 프로그램 내부의 정확한 charset 일치, 리터럴 인덱스로 처리되는 숫자형 gNN/glyphNN 이름, 그리고 TrueType 프로그램의 경우 Adobe Glyph List 이름-유니코드 변환에 이은 cmap 조회입니다. 복합 글꼴의 경우 §9.7에 따라 CIDToGIDMap이 담당합니다: 일반적으로 /Identity 형식이 쓰이지만, CID별로 색인화된 빅엔디안 쌍의 스트림인 경우도 있으며 HotPDF의 자체 유니코드 출력은 압축 서브셋을 위해 바로 이 스트림 형식을 취하므로 스트림 경로는 예외적인 케이스가 아닙니다
// /CIDToGIDMap as a stream: big-endian Word pairs indexed by CID
if 2 * CID + 1 <= High(MapBytes) then
GID := (MapBytes[2 * CID] shl 8) or MapBytes[2 * CID + 1]
else
GID := 0; // out of range maps to .notdef
TrueType cmap 조회가 필요한 경우 HotPDF는 하나의 하위 테이블만 신뢰하지 않고 예비 체인을 탐색합니다: Windows 유니코드 하위 테이블(형식 4, 보조 평면의 경우 형식 12)이 우선이고, 그다음으로 F000 사용자 정의 영역(PUA) 규칙이 하위 바이트로 미러링된 (3,0) 심볼 하위 테이블(Wingdings 같은 심볼 폰트가 일반 ASCII 코드에 응답하는 이유), 마지막으로 레거시 형식 6 및 0 순입니다. 형식 2 하위 테이블은 의도적으로 해석되지 않습니다: 이 테이블은 유니코드가 아닌 Shift-JIS 및 Big5와 같은 기존 멀티바이트 코드 페이지를 매핑하기 때문이며, 어차피 현대 CJK 글꼴들은 형식 4 또는 형식 12 하위 테이블을 함께 제공합니다. 이러한 경로로도 확인되지 않는 모든 코드는 해당 단일 글리프에 대해 GDI 드로잉으로 백다운되므로, 단 하나의 매핑 불가능한 문자로 인해 텍스트 실행 전체가 망가지는 일을 방지합니다
임베디드 경로의 한계점
제약 조건에 대해 명확히 밝혀둘 필요가 있습니다. 힌팅(hinting)은 실행되지 않습니다 — 아웃라인은 작성된 그대로 칠해지며, 150 DPI 이상의 배율에서는 힌트가 적용된 출력과 구별할 수 없지만 매우 작은 크기에서는 힌트 처리된 래스터라이저와 1픽셀 정도 차이가 날 수 있습니다. FontFile 내의 원본 Type 1 프로그램(eexec 암호화 캐릭터스트링)은 해석되지 않으며, OpenType 가변 글꼴(variable-font) 축은 적용되지 않습니다. 손상된 글꼴 프로그램이나 유효한 cmap이 없는 glyf 테이블처럼 두 케이스 모두 렌더링을 멈추는 대신 시스템 글꼴 드로잉으로 백다운됩니다. 이러한 충실도 우선 방식은 렌더러의 다른 부분에도 동일하게 적용됩니다 — 축 및 방사형 셰이딩 패턴도 그래디언트 수준의 처리를 받으며, 생성 측에서도 EndDoc이 폰트 서브셋을 정렬하는 방법에 고유한 이야기가 포함되어 있습니다
이 파이프라인을 사용하기 위해 글꼴 관련 코드를 별도로 작성할 필요가 없습니다 — 위에 설명한 모든 메커니즘은 페이지 렌더링 호출 시 내부에서 자동으로 실행됩니다
var
Pdf: THotPDF;
Bmp: TBitmap;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.LoadFromFile('invoice-embedded-fonts.pdf') > 0 then
begin
// Embedded TrueType, CFF, and Type 3 fonts render from the
// file itself — nothing needs to be installed on this machine
Bmp := Pdf.RenderLoadedPageToBitmap(0, 144);
if Bmp <> nil then
try
Bmp.SaveToFile('page1.bmp');
finally
Bmp.Free;
end;
end;
finally
Pdf.Free;
end;
end;
실제 운영 상의 이점은 명확합니다: 글꼴을 포함한 PDF는 해당 글꼴이 설치되지 않은 빌드 서버, Windows 컨테이너, 혹은 고객의 데스크톱에서도 해당 글꼴을 유지한 채 렌더링됩니다. 임베디드 글리프 렌더링 파이프라인은 외부 DLL 종속성 없이 PDF 생성, 편집, 텍스트 추출 및 페이지 렌더링을 모두 수행하는 네이티브 VCL 라이브러리인 Delphi 및 C++Builder용 HotPDF Component의 일부로 포함되어 제공됩니다