PDFlibPas는 선택한 폰트가 그릴 수 없는 문자를, 설치된 서체들의 폴백 체인을 클러스터 단위로 검색하면서 셰이핑과 양방향 런 순서를 보존한 채로 해결합니다. 이를 켜는 것은 SetAutomaticFontFallback이고, AddFontFallback으로 체인을 확장하며, 실제로 출력에 쓰인 폴백 폰트만 파일에 임베드됩니다
이 기능이 해결하는 문제는 모든 문서 생성기가 템플릿 폰트가 전혀 예상하지 못한 문자 체계로 고객 이름이 들어오는 순간 처음 마주치는 문제입니다. 실패는 조용하며, 바로 그 점이 이를 값비싸게 만듭니다
지원되지 않는 텍스트는 왜 오류를 내는 대신 사라지는가?
PDF에는 문자를 그릴 수 없는 폰트라는 개념 자체가 없기 때문입니다. 단순 폰트는 인코딩을 통해 바이트 코드를 글리프 이름에 매핑하고, 복합 폰트는 CMap을 통해 코드를 글리프 인덱스에 매핑합니다. 서체가 담고 있지 않은 글리프를 요청하면 글리프 인덱스 0, 즉 .notdef를 얻게 되는데, 대부분의 서체는 이를 아무것도 그리지 않거나 빈 상자로 그립니다. 파일은 구조적으로 유효하고, 텍스트 연산자도 형식이 올바르며, 페이지는 렌더링됩니다. 다만 이름이 있어야 할 자리가 비어 있을 뿐입니다
ISO 32000-1의 어떤 조항도 생성기가 이를 알아채야 한다고 요구하지 않습니다. 커버리지를 확인하지 않고 텍스트를 쓰는 생성기는 기술적으로는 규격을 준수하지만 내용을 조용히 잃어버린 PDF를 만들어내고, 그 손실은 몇 주 뒤 고객의 화면에서야 드러납니다. 이것이 폴백 기능과 누락 글리프 리포트가 함께 제공되는 이유입니다. 해결할 수 있는 것을 해결하는 일은 절반일 뿐이고, 해결할 수 없었던 것을 보고하는 일이 나머지 절반입니다
폴백은 코드 포인트 단위가 아니라 클러스터 단위로 일어난다
세분성(granularity)은 제대로 작동하는 구현과 그럴듯해 보이기만 하는 구현을 가르는 디테일입니다. 텍스트는 독립적인 문자들의 나열이 아닙니다. 데바나가리 음절, 피부색 수식자가 붙은 이모지, 결합 부호가 붙은 기본 문자는 각각 하나의 폰트로 렌더링되어야 하는 하나의 클러스터입니다. 그 안의 셰이핑 결정이 해당 서체의 테이블에 의존하기 때문입니다
PDFlibPas는 클러스터 단위로 해결하므로, 폴백 서체가 커버하는 클러스터는 전부 그 서체로 그려집니다. 클러스터 중간을 나눠 절반은 기본 폰트로, 절반은 폴백으로 그리면 기술적으로는 존재하지만 눈에 띄게 깨진 결과가 나오는데, 이는 처음의 빈칸보다 사실상 더 나쁩니다. 런 순서도 보존되므로, 오른쪽에서 왼쪽으로 흐르는 런 안의 폴백이 주변 텍스트의 순서를 뒤바꾸지 않습니다. 같은 메커니즘이 일본어와 중국어의 세로쓰기에서 설명하는 세로 레이아웃의 기반이기도 합니다
var
Lib: TPDFlib;
begin
Lib := TPDFlib.Create;
try
Lib.SetOrigin(1);
Lib.SetAutomaticFontFallback(1);
// 검색 순서: 먼저 일치하는 것이 우선이므로 가장 광범위한 서체를 마지막에 둘 것
Lib.AddFontFallback('Microsoft YaHei'); // 중국어 간체
Lib.AddFontFallback('Meiryo'); // 일본어
Lib.AddFontFallback('Segoe UI Symbol');
Lib.AddFontFallback('Segoe UI Emoji');
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_REPORT);
Lib.AddTrueTypeFont('Arial', 1); // 1 = 서체를 임베드함
Lib.SetTextSize(11);
Lib.DrawText(72, 720, 'Invoice for 北京示例科技有限公司');
Lib.DrawText(72, 700, 'Delivery status: on time');
Lib.SaveToFile('invoice.pdf');
finally
Lib.Free;
end;
end;
체인 순서를 신중하게 정하십시오. 해결 과정은 클러스터를 커버하는 첫 번째 서체를 선택하므로, 광범위한 범유니코드 폰트를 앞에 두면 거의 모든 것을 그 폰트가 가져가 버리고 여러분이 공들여 고른 문자 체계별 서체는 참조조차 되지 않습니다. 특정 서체를 앞에 두고 만능 서체는 마지막에 두십시오
보고인가 중단인가: 어떤 실패 방식을 원하는가?
SetMissingGlyphPolicy는 호환 기본값인 PDF_MISSING_GLYPH_REPORT 또는 PDF_MISSING_GLYPH_ABORT를 받습니다. 리포트 정책에서는 텍스트 연산이 계속 진행되고, 해결할 수 없는 코드 포인트는 예전처럼 버려지되 각각이 기록됩니다. 중단 정책에서는 어떤 콘텐츠도 기록되기 전에 텍스트 연산이 거부되고 LastErrorCode가 521로 설정됩니다
문서가 무엇을 위한 것인지에 따라 선택하십시오. 사내 보고서 배치라면 계속 렌더링하면서 빠진 부분을 기록해야 합니다. 오늘의 다소 불완전한 보고서가 아예 없는 보고서보다 낫기 때문입니다. 법적 구속력이 있는 계약서, 청구서, 그 밖에 이름이 들어가는 무엇이든 중단해야 합니다. 당사자 이름에서 조용히 빠진 문자는 분쟁 중이 아니라 여러분 자신의 프로세스 안에서 발견하고 싶은 결함이기 때문입니다. 중단 정책은 기록 전에 실패하므로, 절반만 작성된 콘텐츠 스트림이 남지 않습니다
var
Lib: TPDFlib;
Report: WideString;
begin
Lib := TPDFlib.Create;
try
Lib.SetMissingGlyphPolicy(PDF_MISSING_GLYPH_ABORT);
// ... 문서를 작성 ...
if Lib.DrawText(72, 660, CustomerName) <> 1 then
if Lib.LastErrorCode = PDFLIB_ERROR_MISSING_GLYPH then
begin
Report := Lib.GetMissingGlyphReportJSON;
// {"valid":false,"policy":1,"eventCount":1,"events":[
// {"sequence":1,"documentIndex":0,"page":1,"utf16Index":12,
// "codePoint":21271,"unicode":"U+5317","fontName":"Arial",
// "fontType":"TrueType","operation":"DrawText"}]}
EscalateToOperator(Report);
end;
finally
Lib.Free;
end;
end;
이 리포트는 의도적으로 기계가 읽기 쉽고 크기가 제한되어 있습니다. 각 이벤트는 페이지, 문자열 안의 UTF-16 인덱스, 숫자와 U+XXXX 형식 모두로 표현된 코드 포인트, 선택된 폰트, 그 종류, 문제가 발생한 연산을 담고 있어서, 지원 티켓이 증상을 서술하는 대신 정확한 문자를 특정할 수 있습니다. 추적기는 가장 최근 256개의 이벤트를 보관하는데, 이는 문서를 진단하기에는 충분하면서도 병적인 실행이 진단 정보를 메모리 문제로 바꿔놓기에는 충분히 작은 크기입니다
측정과 드로잉은 반드시 일치해야 한다
너비 측정은 드로잉과 동일한 클러스터 인식 폴백 결정을 사용합니다. 당연해 보이지만, 자체 구현한 폴백 레이어 대부분이 틀리는 지점이 바로 여기입니다. 드로잉 경로만 패치하고 측정은 기본 폰트에 남겨두는 바람에, 모든 텍스트 상자, 오른쪽 정렬, 표 열이 실제로 렌더링된 것과 일치하지 않는 너비로부터 계산되고 맙니다
두 경로가 같은 해결 과정을 공유하기 때문에, 그리기 전에 측정한 문자열은 폴백 런을 포함해 측정된 그 너비를 정확히 차지합니다. 바로 이 점이 여러분이 직접 감사한 곳에서만이 아니라 전역적으로 폴백을 켜도 안전하게 만들어줍니다
사용한 것만 임베드된다
폴백 폰트는 지연 임베드됩니다. 체인 안에서 어떤 클러스터도 해결한 적 없는 서체는 출력물에 아무것도 기여하지 않습니다. 중국어 문자 하나와 라틴 문자 5,000개를 담은 문서는 완전한 CJK 서체를 싣지 않습니다. 그 문서가 싣는 것은 그 글리프 하나를 위해 서브셋 처리 과정이 만들어낸 결과물뿐이며, 이는 파일 크기 최적화와 폰트 서브셋화에서 설명하는 동작입니다
이 지연성 덕분에 폭넓은 체인을 구성하는 비용이 저렴해집니다. 여러분이 서비스하는 모든 로케일에 걸쳐 문서 집합이 필요로 할 만한 서체를 등록해 두면, 개별 PDF는 실제로 사용한 만큼만 비용을 치릅니다. 여러분이 생성하지 않은 문서, 즉 빠진 서체가 이미 기존 파일 안에 있는 경우라면 복구 경로가 다르며, 기존 PDF에 누락된 폰트 임베드하기에서 다룹니다
배포와 관련해 분명히 짚어둘 만한 주의사항이 하나 있습니다. 폴백은 코드를 실행하는 머신에 설치된 서체를 대상으로 해결됩니다. CJK 폰트가 설치되지 않은 서버에는 폴백할 대상이 없으며, 첫 번째 항의가 들어온 뒤가 아니라 첫 번째 문서에서 리포트가 이를 알려줄 것입니다. 의존하는 폰트는 함께 배포하고, 그것을 임베드하는 것에 대한 라이선스도 확인해 두십시오
PDFlibPas는 Delphi, C++Builder, Lazarus용 PDF 라이브러리이며 이에 대응하는 DLL과 ActiveX 인터페이스도 갖추고 있어서, 폴백과 누락 글리프 API를 Pascal이 아닌 호출자에서도 사용할 수 있습니다. 전체 문서는 PDFlibPas Delphi PDF 라이브러리 페이지에 있습니다