PDF Library for Delphi는 웹 프로젝트가 이미 사용 중인 폰트를 그대로 받아들입니다. WOFF2 컨테이너를 가져오고, 가변 폰트의 디자인 축을 JSON으로 열거하며, 선택된 축 좌표 집합을 변형에 대해 아무것도 모르는 일반적인 임베딩, 서브셋팅, 추출 경로가 처리할 수 있는 정적 폰트로 인스턴스화합니다. TrueType 윤곽선과 CFF2 윤곽선 모두 지원됩니다
이것이 다루는 상황은 흔하면서도 성가신 문제입니다. 브랜드의 타이포그래피가 디자인 시스템에 정의되어 있고, 그 디자인 시스템은 가변 WOFF2 파일을 제공하는데, PDF 생성기는 그 컨테이너도 변형 데이터도 읽지 못합니다. 흔한 우회책 — 실제로 사용되는 모든 굵기의 정적 TTF 인스턴스를 폰드리에 요청하는 것 — 은 몇 주가 걸리고 아무도 동기화 상태로 유지하지 않는 파일들을 만들어냅니다
왜 WOFF2는 단순히 압축된 폰트가 아닌가?
WOFF2를 Brotli 래퍼로 취급해 압축을 풀면 유효한 sfnt가 아닌 바이트가 나옵니다. 이 형식은 변환되고 재정렬된 테이블도 허용합니다: glyf, loca, hmtx는 폰트를 파싱 가능하게 만들기 전에 되돌려야 하는 재배열된 형태로 저장될 수 있습니다. 범용 압축 해제기는 이를 할 수 없습니다
Windows에서는 Windows 10 버전 1703부터 플랫폼이 컨테이너 분석과 압축 해제를 제공해 왔으므로, 라이브러리는 또 다른 디코더를 함께 배포하는 대신 시스템 구현을 사용합니다. 이 결정 덕분에 배포에 추가 DLL이 필요 없고, 변환 되돌리기는 운영체제 자체의 텍스트 스택이 사용하는 것과 같은 코드가 처리합니다. 다른 플랫폼에서는 WOFF1은 여전히 순수 Pascal 경로를 거치며 WOFF2는 명시적으로 지원되지 않는다고 보고하는데, 이는 조용히 손상된 폰트를 내놓는 것보다는 정직한 답입니다
가져오기 경로는 디코딩 전과 후 모두 검증합니다: 48바이트 헤더, 컨테이너 길이, 예약 필드, 선언된 sfnt 크기, 512MiB 상한이 먼저 모두 확인되며, 디코딩된 스트림 크기는 이후 선언값과 대조됩니다. 웹 폰트는 다운로드된 콘텐츠이며, 막을 가치가 있는 두 가지 실패 모드는 크기 불일치와 압축 폭탄입니다
uses
PDFlibrary;
var
Lib: TPDFlib;
FontID, InstanceID: Integer;
AxesJSON: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.NewDocument;
Lib.SetPageSize('A4');
Lib.NewPage;
// WOFF2, WOFF1, TTF, OTF, TTC 모두 같은 호출로 들어감
FontID := Lib.AddOpenTypeFontFromFile('Inter.woff2', 0);
if FontID <= 0 then
raise Exception.Create('font could not be imported');
// 이 폰트가 실제로 할 수 있는 것은 무엇인가?
if Lib.GetFontVariationAxes(FontID, AxesJSON) = 1 then
Log(AxesJSON); // 축별 태그, 최소값, 기본값, 최대값
// 하나의 디자인 위치를 정적 폰트로 굽기
InstanceID := Lib.CreateOpenTypeFontVariation(FontID,
'wght=620,wdth=87.5');
Lib.DrawOpenTypeText(56, 760, InstanceID, 24, 'Quarterly results', 0);
Lib.SaveToFile('report.pdf');
finally
Lib.Free;
end;
end;
인스턴스화가 다시 만들어야 하는 것과 제거하는 것
가변 폰트는 글리프마다 하나의 기본 윤곽선과, 디자인 공간 안의 위치로 색인된 델타를 저장합니다. 정적 인스턴스를 만든다는 것은 요청된 좌표에서 그 델타들을 보간하고, 점들을 이동시킨 다음, 거기서 파생되는 모든 것을 다시 계산한다는 뜻입니다
TrueType 윤곽선의 경우 이는 fvar, avar, gvar, HVAR, MVAR을 파싱하고, 공유 및 전용 점 번호를 가진 튜플 델타를 적용하며, 명시적인 델타가 없는 점들을 보간하는 것을 뜻합니다. 수평 팬텀 포인트는 HVAR과 함께 advance width와 왼쪽 사이드 베어링을 결정합니다. 복합 글리프는 먼저 구성 요소 의존성과 점 매핑을 해석한 다음, 이동, 스케일, 행렬, 부착 오프셋을 적용해야 합니다 — 악의적인 구성 요소 그래프가 끝없이 재귀하지 않도록 깊이 제한과 순환 감지도 함께 적용됩니다
메타데이터도 함께 이동하는데, 이 부분을 대부분의 구현이 건너뜁니다. MVAR은 hhea, OS/2, post 안의 전역 메트릭을 갱신하므로, 줄 간격, 밑줄 위치, 이탤릭 각도가 기본값이 아니라 인스턴스와 일치하게 됩니다. wght, wdth, ital, slnt 축은 여기에 더해 굵기 등급, 너비 등급, 선택 플래그, macStyle, 이탤릭 각도를 갱신하므로, 정적 폰트를 검사하는 도구는 사용자가 요청한 스타일을 그대로 보고하게 됩니다. 형태는 볼드이면서 메타데이터는 레귤러라고 말하는 폰트는 이후의 모든 곳에서 잘못 표시될 것입니다
이후 출력은 복합 윤곽선을 평탄화하고 더 이상 적용되지 않는 테이블 — 변형 테이블, 힌트, 디바이스 테이블, 그리고 모든 서명 — 을 제거합니다. 글리프 경계, hhea 요약 메트릭, maxp 힌트 상한, 테이블별 체크섬, 폰트 전체 체크섬이 모두 다시 계산됩니다
CFF2는 같은 목적지에 다른 경로로 도달한다
CFF2는 PDF 폰트 프로그램으로 직접 임베드될 수 없습니다: 그 charstring에는 폭 정보가 없고, 글리프 순서와 메트릭을 주변 OpenType 테이블에 의존합니다. 그래서 CFF2 폰트를 인스턴스화하면 대신 CID로 키가 지정된 CFF OpenType 폰트가 만들어지며, 이는 기존의 그리기, 임베딩, 서브셋팅 경로가 이미 처리할 수 있는 형태입니다
CFF2 폰트를 로드하면 즉시 현재 문서를 위한 기본 좌표 정적 인스턴스가 만들어지는 한편, 원본 CFF2 sfnt는 폰트 객체에 그대로 유지되어 축 열거와 이후의 인스턴스화가 여전히 전체 디자인 공간을 볼 수 있습니다. 각 글리프는 인스턴스화 중에 개별적으로 서브루틴이 제거되는데, 이는 변형 연산을 완전히 없애고 기존 서브셋터가 변경 없이 동작하게 해 줍니다. 그 대가는 공유 서브루틴을 유지한 것보다 정적 인스턴스가 조금 더 커질 수 있다는 것인데, 두 번째 서브셋팅 구현이 필요 없다는 점을 생각하면 합당한 대가입니다
신뢰하기 전에 인스턴스 검증하기
인스턴스화는 어떤 예외도 보고하지 않으면서 미묘하게 잘못될 수 있는 종류의 기능입니다. 검사 세 가지가 거의 모든 것을 잡아냅니다
인스턴스를 내보내 폰트 도구에서 열어 보세요. SaveFontToFile은 보관된 폰트 프로그램을 기록하므로, 메모리 안의 인스턴스를 PDF를 먼저 만들지 않고도 덤프할 수 있습니다. 내보낸 파일은 깔끔하게 파싱되어야 하고, 변형 테이블을 담고 있지 않아야 하며, 요청한 스타일 메타데이터를 그대로 보고해야 합니다
기본값이 아닌 위치에서 advance를 비교하세요. 파이프라인이 같은 인스턴스를 다른 방식으로도 만들 수 있다면, advance width와 곡선 경계를 글리프별로 비교하세요. 여기서 참조 구현은 313개 글리프, 2축 폰트의 독립적인 인스턴스화와 대조했을 때 모든 글리프에서 최대 차이가 0이었습니다
그리고 생성된 PDF에서 텍스트를 다시 추출해 보세요. 임베드는 되지만 추출 가능한 텍스트를 만들어내지 못하는 인스턴스는 ToUnicode 매핑을 잃은 것이며, 이는 시각적 검사를 통과한 뒤로도 오랫동안 검색과 접근성에 영향을 미칩니다. 더 넓은 범위의 추출 경로는 텍스트, 이미지, 폰트 추출에서 다룹니다
// 검사나 재사용을 위해 생성된 인스턴스를 덤프함
Lib.SaveFontToFile('Inter-620-87.ttf');
// 선언된 범위 밖의 좌표를 요청하면 클램프되므로,
// 폰트의 한계를 짐작하지 말고 축 JSON을 읽을 것
// {"axes":[{"tag":"wght","min":100,"default":400,"max":900}, ...]}
브랜드 폰트 파이프라인을 위한 실무 참고 사항
좌표 집합별로 인스턴스를 캐시하세요. 서로 다른 좌표 집합은 각각 별개의 폰트 프로그램이므로, 굵기 400과 굵기 620을 함께 쓰는 문서는 하나의 공유 폰트가 아니라 두 개의 서브셋을 임베드합니다. 같은 인스턴스를 반복해서 생성하는 것은 낭비된 작업이고, 거의 비슷한 굵기를 수십 개 생성하는 것은 낭비된 파일 크기입니다
그릴 때 사용할 바로 그 인스턴스로 측정하세요. 압축된 인스턴스는 기본값보다 advance가 좁으므로, 인스턴스화 이전에 측정한 레이아웃은 잘못된 너비를 측정한 것입니다. 이는 프로덕션에서만 텍스트가 상자를 넘치는 가장 흔한 단일 원인입니다
브랜드 폰트에 없는 글리프를 위한 폴백 경로를 유지하세요. 가변 브랜드 폰트는 보통 라틴 문자만 다루고 그 밖에는 거의 다루지 않으므로, 키릴 문자 고객 이름이 들어간 보고서는 그렇지 않으면 글리프 누락을 보이게 됩니다. 자동 폴백 메커니즘은 누락된 글리프를 위한 자동 폰트 폴백에서 설명합니다. 그리고 목표가 특정 굵기가 아니라 더 작은 파일일 때는, 서브셋팅이 인스턴스화보다 더 많은 일을 해 주며, 이는 PDF 파일 크기 최적화와 폰트 서브셋팅에서 다룹니다
웹 폰트 가져오기, 가변 인스턴스화, 서브셋팅, 폴백은 Delphi, C++Builder, Free Pascal을 위한 동일한 폰트 파이프라인의 일부입니다. 전체 기능 목록은 Delphi용 PDF Library 페이지에서 확인할 수 있습니다