PDF에는 가변 폰트라는 개념이 없습니다. PDF 파일에 임베드되는 폰트는 고정된 윤곽선과 고정된 메트릭을 가진 집합이므로, 가변 폰트는 문서에 들어가기 전에 하나의 정적 인스턴스로 축소되어야 합니다. HotPDF는 이 인스턴싱을 내부에서 수행합니다. 가변 폰트의 축을 살펴본 다음 굵기 620이나 너비 87.5 같은 좌표를 선택하면, 라이브러리가 그 값들을 완전하고 독립적인 폰트 프로그램에 구워 넣어 모든 규격 준수 PDF 리더가 렌더링할 수 있게 만듭니다
이 문제가 중요한 이유는 이론적이라기보다 실용적입니다. 서체 제작사들은 점점 더 열댓 개의 정적 굵기 대신 하나의 가변 파일을 배포하고 있고, 디자인 팀은 어떤 명명된 인스턴스도 제공하지 않는 값을 고르곤 합니다. 인스턴싱이 없으면 리포트 생성기는 디자인 결정을 무시하고 기본 인스턴스로 후퇴하거나, 가변 폰트 전체를 임베드한 채 뷰어가 알 방법도 없는 축 좌표를 알아서 존중해 주기를 바랄 수밖에 없는데, 어떤 리더도 그렇게 해야 할 의무는 없습니다
인스턴싱은 실제로 무엇을 재구축해야 할까?
OpenType 가변 폰트는 글리프마다 기본 윤곽선 하나와 디자인 공간 위치별로 색인된 델타 집합을 저장합니다. 축 좌표를 적용하는 일은 헤더에 숫자 하나를 써넣는 문제가 아닙니다. gvar 테이블을 순회하며 요청된 위치에 대한 델타를 보간하고, 포인트를 이동시킨 다음, 그 포인트들로부터 파생된 모든 값을 다시 계산해야 합니다. HotPDF는 글리프 윤곽선, long 형식의 loca 테이블, 완전한 가로/세로 메트릭, 전역 폰트 바운딩 박스, sfnt 체크섬 보정값을 다시 만들어냅니다
무엇을 제거해야 하는지도 그만큼 중요합니다. 정적 인스턴스는 fvar, avar, gvar, HVAR, VVAR, MVAR, STAT, cvar를 남겨서는 안 되며, 오래된 DSIG도 함께 제거해야 하는데 서명된 바이트가 더 이상 존재하지 않기 때문입니다. 이 중 무엇이라도 남겨두면 가변 폰트라고 주장하면서 실제로는 이미 이동된 윤곽선을 담고 있는 폰트가 만들어지고, 변형을 실제로 적용하는 리더는 그 변형을 두 번 적용하게 됩니다
팬텀 포인트, 그리고 이중 적용의 함정
전체 과정에서 가장 미묘한 규칙은 메트릭에 관한 것입니다. gvar에서 글리프 하나의 포인트 개수는 윤곽선 포인트, 또는 합성 글리프의 경우 컴포넌트 포인트에 더해 왼쪽 사이드 베어링, 어드밴스 폭, 그리고 그 수직 대응값을 인코딩하는 팬텀 포인트 네 개를 포함합니다. 이 팬텀 포인트들 역시 델타의 대상이 됩니다
그래서 폰트에 gvar 테이블이 있으면, HotPDF는 보간된 팬텀 포인트에서 가로/세로 메트릭을 유도하고 HVAR이나 VVAR을 추가로 적용하지 않습니다. 둘 다 적용하는 것이 전형적인 오류로, 같은 변형이 두 번 적용되어 모든 어드밴스 폭이 아주 조금씩 넓게 나오고, 이는 정렬된 텍스트 줄에서 텍스트가 점점 오른쪽으로 밀려나는 형태로 드러납니다. 폰트에 gvar이 없을 때만 라이브러리는 메트릭 변형 저장소를 hmtx나 vmtx에 직접 구워 넣습니다
지오메트리를 정직하게 유지해 주는 세부 사항이 두 가지 더 있습니다. 팬텀 포인트는 윤곽선 보간에 절대 참여하지 않으므로, 단순 글리프에서 명시적으로 나열되지 않은 포인트는 팬텀 포인트를 제외한 채 윤곽선별 IUP로 추론됩니다. 그리고 합성 글리프는 XY 매개변수를 사용하는 컴포넌트 오프셋에 델타가 적용된 뒤, 자식 바운드가 재귀적으로 다시 계산됩니다. 이 재귀는 깊이가 제한되어 있고 순환도 검사되는데, 악의적이거나 단순히 잘못된 컴포넌트 그래프가 그렇지 않으면 끝없이 재귀할 수 있기 때문입니다
선택하기 전에 디자인 공간 살펴보기
인스턴싱 워크플로에서 가장 먼저 호출하는 함수는 InspectVariableFont이며, 서체 제작사가 정의한 축과 명명된 인스턴스를 보고합니다. 축 레코드는 4바이트 태그, 최소값, 기본값, 최대값, 플래그, 이름 ID를 담고, 명명된 인스턴스는 서브패밀리 이름 ID, 플래그, 선택적인 PostScript 이름 ID, 축마다 하나씩의 좌표를 담습니다:
var
Pdf: THotPDF;
Axes: THPDFVariableFontAxisArray;
Instances: THPDFVariableFontNamedInstanceArray;
I: Integer;
begin
Pdf := THotPDF.Create(nil);
try
if Pdf.InspectVariableFont('C:\Fonts\Inter.ttf', Axes, Instances) then
begin
for I := 0 to High(Axes) do
Writeln(Format('%s min=%.1f default=%.1f max=%.1f',
[string(Axes[I].Tag), Axes[I].MinimumValue,
Axes[I].DefaultValue, Axes[I].MaximumValue]));
Writeln(Format('%d named instance(s) defined', [Length(Instances)]));
end
else
Writeln('not a variable font - embed it as an ordinary TrueType face');
finally
Pdf.Free;
end;
end;
축 범위를 보고하는 것이 중요한 이유는 축 값이 UI가 제공하는 범위가 아니라 폰트가 선언한 범위로 클램프되기 때문입니다. wght 축이 900에서 멈추는 폰트에서 사용자가 굵기 1000을 요청하도록 허용하는 슬라이더가 있다면, 그 보정은 폰트 레이어에서 조용히 처리할 게 아니라 인터페이스에서 해야 합니다. 그러지 않으면 인쇄된 결과물이 미리보기와 어긋나게 됩니다
좌표 선택과 문서 출력
축 선택은 상태를 가지며 이후 등록되는 폰트에 적용됩니다. SetVariableFontAxis는 4바이트 인쇄 가능 ASCII 태그와 유한한 값을 받으며, 그 외의 값은 조용히 무시하는 대신 예외로 거부합니다. ClearVariableFontAxes는 선택 상태를 초기화하고, GetVariableFontAxisSelections는 현재 대기 중인 값을 보고하는데, 같은 문서 객체를 여러 코드 경로가 건드릴 수 있는 리포트 엔진에서는 이를 로그로 남길 만합니다. 패밀리 자체는 다른 임베드된 TrueType 페이스와 마찬가지로 SetFont를 통해 이름으로 선택됩니다:
begin
Pdf := THotPDF.Create(nil);
try
Pdf.BeginDoc;
Pdf.SetVariableFontAxis('wght', 620); // semibold, not a named instance
Pdf.SetVariableFontAxis('wdth', 87.5); // slightly condensed
Pdf.CurrentPage.SetFont('Inter', [], 11);
Pdf.CurrentPage.TextOut(72, 720, 0, 'Quarterly results');
Pdf.ClearVariableFontAxes; // back to the default instance
Pdf.CurrentPage.SetFont('Inter', [], 10);
Pdf.CurrentPage.TextOut(72, 700, 0, 'Prepared by the finance team');
Pdf.EndDoc;
finally
Pdf.Free;
end;
end;
OnVariableFontInstance 이벤트는 인스턴스가 만들어질 때마다 발생하며 그때 사용된 축 값을 보고하는데, 이는 특정 PDF에 실제로 무엇이 들어 있는지 로그로 증명하는 가장 저렴한 방법입니다. 서로 다른 좌표 집합은 각각 서로 다른 폰트 프로그램을 만들어내므로, 축 선택을 폰트 캐시 키의 일부로 취급하십시오. 캐싱 메커니즘은 영속적 폰트 서브셋 캐시에서 설명합니다
인스턴싱은 서브셋팅 및 셰이핑과 어떻게 상호작용할까?
인스턴싱은 서브셋팅보다 먼저 실행되며, 그 순서가 올바릅니다. 인스턴싱된 폰트는 일반적인 정적 TrueType 페이스이므로, 보통의 서브셋터는 다른 모든 폰트와 마찬가지로 다룹니다. 글리프 클로저를 계산하고, 문서가 실제로 사용하는 글리프만 남긴 뒤 나머지는 버립니다. 유의해야 할 상호작용은 같은 패밀리의 서로 다른 두 축 선택은 서로 다른 두 폰트 프로그램이라는 점입니다. 즉 굵기 400과 굵기 620을 섞어 쓰는 문서는 인스턴스 두 개를 가진 공유 페이스 하나가 아니라 두 개의 서브셋을 임베드합니다
셰이핑은 원리상 영향을 받지 않지만 실제로 확인해 볼 가치는 있습니다. 레이아웃 기능은 GSUB과 GPOS에 존재하며 인스턴싱은 이를 그대로 보존하므로, OpenType GSUB 스타일 대체 글리프에서 설명한 대로 합자와 스타일 대체 글리프는 계속 동작합니다. 달라지는 것은 위치 지정입니다. 콘덴스트 인스턴스는 기본값보다 어드밴스 폭이 좁으므로, 인스턴싱 이전에 텍스트를 측정한 레이아웃은 잘못된 폭을 측정한 것입니다. 렌더링할 때와 같은 축 선택으로 측정하면 이 불일치는 사라집니다
이 경로를 확장하려는 사람에게 유용한, 구현에서 나온 마지막 방어적 참고 사항이 있습니다. 세로 메트릭이 없는 폰트도 Delphi 호출 지점에서 동적 배열 인수를 여전히 평가하므로, 파스 타임 배열은 빈 인덱스를 우회하는 HasVerticalMetrics 검사에 의존하지 않고 항상 할당됩니다. 겉보기엔 안전해 보이는 분기가 정확히 테스트하지 않은 폰트에서 액세스 위반으로 바뀌는, 언어 수준의 세부 사항입니다
가변 폰트 지원은 임베딩, 서브셋팅, 글리프 클로저와 같은 폰트 파이프라인 안에 있으며, 이는 폰트 서브셋 클로저와 셰이핑된 글리프에서 더 깊이 다룹니다. Delphi와 C++Builder를 위한 전체 타이포그래피 기능 목록은 HotPDF Delphi PDF 컴포넌트 페이지에 정리되어 있습니다