PDF에서 빠진 글리프는 오류가 아닙니다. 생산자가 선택된 폰트가 매핑할 수 없는 문자를 요청하면 폰트는 글리프 인덱스 0을 돌려주고, 나오는 파일은 구조적으로 유효하고 어디서든 열리며 이름이나 금액이 있어야 할 자리에 빈 상자를 보여 줍니다. 생성 파이프라인의 누구도 알아차리지 못합니다. 수신자가 알아차립니다. HotPDF는 TrackUnresolvedGlyphs로 이 순환을 닫습니다. 켜면 텍스트 그리기 경로가 글리프 조회가 인덱스 0으로 귀결되는 모든 코드 포인트를 기록하고, 코드 포인트, 실패한 폰트, 속한 스크립트, 이를 커버할 폰트 제안을 곁들여 고유한 발견마다 OnUnresolvedGlyph를 한 번씩 발화합니다
탐지가 답의 절반입니다. 나머지 절반은 SetFontFallbackChain으로, 스크립트별로 순서 있는 폰트 목록을 등록해 흔한 사례는 스스로 해결되고 진짜 간극만 핸들러에 닿습니다. 둘이 함께 고객이 보고하던 부류의 결함을 빌드 시점 검사로 바꿉니다
빠진 글리프는 왜 아무것도 발생시키지 않는가
ISO 32000은 생산자에게 커버리지 검증 의무를 부과하지 않고 글리프 인덱스 0은 정당한 글리프이기 때문입니다. 이는 .notdef이며, 그 외곽선은 폰트 디자이너가 정합니다. 보통 빈 또는 속이 빈 사각형이고 가끔 아무것도 아닙니다. 이를 그리는 뷰어는 올바르게 행동하고 있는 것입니다. 텍스트 추출은 오히려 올바른 문자를 돌려줄 수도 있습니다. /ToUnicode 매핑은 외곽선이 아니라 원본 텍스트에서 작성되므로, 보이는 텍스트에 구멍이 뚫린 문서도 자동화 왕복 검사는 쾌적하게 통과시킵니다
실무적 결과는 커버리지를 그리는 순간에 검사해야 한다는 것입니다. 라이브러리가 아직 어떤 코드 포인트가 요청되었고 폰트가 실제로 어떤 글리프를 내놓았는지 아는 바로 그때입니다. 그 후에는 정보가 사라집니다
탐지기는 디바이스 컨텍스트가 아니라 subset 상태를 봐야 합니다
첫 구현이 틀린 지점이며, 그 이유는 텍스트 파이프라인에 덧대어진 모든 커버리지 검사에 적용되므로 이해할 가치가 있습니다. HotPDF에는 두 텍스트 경로가 있습니다. 하나는 등록 시점에 만든 메모리 내 문자 지도를 갖춘 등록된 Unicode TrueType 폰트를 통해 내보냅니다. 다른 하나는 문자 런마다 새 디바이스 컨텍스트와 폰트 핸들을 만드는 구식 GDI 경로입니다
GDI 경로에서 커버리지를 판단하는 것은 희망이 없습니다. 그 매핑은 내보내지는 콘텐츠 스트림에 최종적으로 들어가는 매핑이 아니고 둘은 동기화되지 않으므로, GDI 결과를 읽는 탐지기는 인쇄 가능 ASCII 범위 전체를 미해결로 보고합니다. 권위 있는 답은 등록된 폰트에 삽니다. RegisterUnicodeTTF가 해석한 문자 지도를 GetUnicodeGlyphForCodepoint로 조회하는 것입니다. 따라서 탐지기는 어떤 GDI 조건도 아니라 subset 준비 상태로 게이트되며, Unicode 폰트를 한 번도 등록하지 않은 문서에서는 아예 돌지 않습니다. 그 문서들은 어차피 표준 인코딩으로 제한되므로 그것이 맞습니다
두 번째 함정이 바로 옆에 앉아 있습니다. 폰트의 GDI 패밀리 이름과 등록 시점에 폰트 바이너리에서 추출한 PostScript 이름은 다른 문자열이며, 정규화할 수 있는 방식이 아닙니다. Arial Unicode MS라 불리는 패밀리는 ArialMT라는 PostScript 이름을 실어 가고 있습니다. "현재 선택된 폰트가 우리가 등록한 것인가"를 이름으로 비교해 쓴 어떤 게이트든 절대 발화하지 않는 죽은 코드입니다. 상태로 게이트하지 폰트 이름으로 게이트하지 마십시오
글리프 탐지기를 emoji로 테스트하지 마십시오
당연한 테스트 사례는 웃는 얼굴이지만, 그것은 탐지기가 망가졌다고 확신하게 만들 것입니다. 점성면(astral plane)의 흔한 emoji 코드 포인트는 곧바로 글리프 인덱스로 매핑하는 private-use 합성 경로를 통해 귀결되므로 일반 커버리지 분기에 닿지 않습니다. 탐지기는 올바르게 행동하는 중이고 테스트가 잘못된 경로를 측정하고 있는 것입니다
대신 배정되지 않은 코드 포인트를 쓰십시오. U+0378은 Unicode에서 영구 배정되지 않으므로 어떤 폰트도 정당하게 매핑할 수 없고, 검증하고 싶은 바로 그 분기를 구동합니다. "기능이 망가졌다"와 "테스트가 기능을 우회하는 입력을 골랐다"의 구분은 실제 몇 시간을 잡아먹으며, 배정되지 않은 코드 포인트가 그것을 피하는 가장 값싼 방법입니다
type
TCoverageAudit = class
private
FFindings: TStringList;
public
procedure Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
property Findings: TStringList read FFindings;
end;
procedure TCoverageAudit.Handle(Sender: TObject;
const Info: THPDFUnresolvedGlyphInfo);
begin
// 발생마다가 아니라 고유 코드 포인트마다 한 번 발화합니다
FFindings.Add(Format('U+%.4X missing in %s (script %d), try: %s',
[Info.CodePoint, String(Info.FontName), Ord(Info.Script),
String(Info.SuggestedFonts)]));
end;
// 생성 작업에 배선하기
Pdf := THotPDF.Create(nil);
try
Pdf.TrackUnresolvedGlyphs := True;
Pdf.OnUnresolvedGlyph := Audit.Handle;
Pdf.RegisterUnicodeTTF('C:\Windows\Fonts\arial.ttf');
Pdf.BeginDoc;
Pdf.CurrentPage.SetFont('Arial', [], 11);
Pdf.CurrentPage.TextOut(50, 720, 0, CustomerName);
Pdf.EndDoc;
if Audit.Findings.Count > 0 then
// 상자가 놓인 페이지를 배송하는 대신 작업을 실패시킵니다
raise Exception.Create(Audit.Findings.Text);
finally
Pdf.Free;
end;
폴백 사슬은 폰트별이 아니라 스크립트별입니다
폴백이 원본 폰트가 아니라 스크립트로 범위가 정해지는 이유는 커버리지 간극이 문자 체계별로 뭉치기 때문입니다. Latin 텍스트 폰트는 Devanagari, 태국어, Han, emoji를 한꺼번에 빠뜨리고, 각각의 대체물은 서로 다른 폰트입니다. 따라서 스크립트별 사슬 하나씩을 선언하는 것이 실제 배포를 기술합니다. 본문용 Latin 폰트 하나, CJK 폰트 하나, emoji 폰트 하나, 만능(catch-all) 하나입니다
// THPDFFontScript는 hfsCommon, hfsLatin, hfsGreek, hfsCyrillic,
// hfsHebrew, hfsArabic, hfsIndic, hfsSoutheastAsian, hfsCJK, hfsKana,
// hfsHangul, hfsEmoji와 hfsOther를 커버합니다
Pdf.SetFontFallbackChain(hfsCJK,
['Microsoft YaHei', 'SimSun', 'Yu Gothic']);
Pdf.SetFontFallbackChain(hfsArabic, ['Segoe UI', 'Arial']);
Pdf.SetFontFallbackChain(hfsEmoji, ['Segoe UI Emoji']);
Pdf.SetFontFallbackChain(hfsOther, ['Arial Unicode MS']);
폴백과 탐지는 대안이 아니라 상호 보완입니다. 사슬은 예상한 커버리지를 처리하고, 탐지기는 예상하지 못한 커버리지를 보고합니다. 임의의 고객 데이터를 처리하는 시스템에서 흥미로운 절반이 바로 후자입니다. 폰트를 대체하면 메트릭이 바뀌므로 폴백한 단락은 리플로할 수 있습니다. 레이아웃이 중요하다면 대체 폰트의 클로저와 서브세팅 동작을 폰트 subset 클로저 문서에서 읽어 볼 가치가 있고, 재정렬이나 접합이 필요한 스크립트는 복합 스크립트 텍스트 셰이핑에서 설명하는 shaping 단계가 처리합니다
기존 경로를 위험에 빠뜨리지 않고 동작을 소급하는 방법
같은 릴리스에는 쌍 간격을 위한 구식 kern 테이블 폴백도 추가되었고, 그것의 범위 잡는 방식은 베껴 둘 가치가 있는 패턴입니다. 커닝 로직에 새 판단 지점을 더하는 대신, 폴백은 GPOS 테이블이 없는 폰트를 위해 이미 존재하던 조기 탈출 분기에 매달렸습니다. GPOS를 갖춘 현대 폰트는 그곳에 절대 닿지 않으므로, 동작이 테스트가 아니라 구조적으로 변하지 않습니다. Unicode 폰트를 등록하지 않는 경로는 0 오프셋 둘을 내므로 역시 변하지 않습니다
이것이 성숙한 렌더링 라이브러리에서 저위험 소급의 일반적인 모양입니다. 지금 아무것도 내놓지 않는 분기를 찾아 새 동작을 그곳에 두는 것입니다. 이것은 "아무것도 퇴보시키지 않았다고 믿는다"를 "이것은 아무것도 퇴보시킬 수 없었다"로 바꾸며, 남의 청구서가 통과하는 텍스트 엔진에 관해서는 훨씬 더 나은 말입니다
로그가 아니라 게이트로 만듭니다
커버리지 발견은 무언가가 그 위에서 실패할 때만 유용합니다. 문서 생성 서비스에서 성과를 내는 배열은 실제 고객 이름, 주소, 제품 설명 코퍼스를 상대로 야간 회귀 작업에서 추적을 계속 켜 두고, 어떤 발견이든 작업을 실패시키는 것입니다. 이벤트는 발생마다가 아니라 고유 코드 포인트마다 한 번 발화하므로, 스크립트 전체가 빠졌을 때조차 출력은 읽을 만한 크기를 유지합니다
프로덕션에서는 같은 핸들러를 텔레메트리로 쓰는 편이 낫습니다. 코드 포인트와 폰트를 기록하고, 문서 제공은 계속하며, 다음에 배포 폰트 세트에 어느 스크립트를 더할지는 집계가 말하게 합니다. 내장 및 대체 폰트의 렌더링 동작은 내장 폰트 글리프 렌더링에서 더 다루고, TrackUnresolvedGlyphs를 포함한 전체 프로퍼티 목록은 HotPDF Delphi PDF component 제품 페이지에 문서화되어 있습니다