PDFium Component는 PDF가 임베드하지 않은 폰트를 참조할 때 어떤 폰트 바이트를 쓸지 Delphi 애플리케이션이 결정하게 해 줍니다. ConfigureSystemFontProvider는 IPdfSystemFontProvider 구현을 설치하며, 이 구현은 페이스 이름, 굵기, 이탤릭 여부, 문자셋, 피치 패밀리까지 담긴 PDFium의 모든 폰트 매핑 요청을 받아 사용할 TrueType, TrueType Collection, OpenType 바이트로 응답합니다
이 기능이 존재하는 이유는 임베드되지 않은 폰트가 일종의 렌더링 복권이기 때문입니다. Arial을 지정하고 아무것도 임베드하지 않은 PDF는 워크스테이션에서는 Arial로, Linux 서버에서는 메트릭 호환 대체 폰트로, 잠금 처리된 컨테이너 이미지에서는 호스트 매퍼가 찾아낸 아무 폰트로 렌더링됩니다. 같은 인보이스가 각기 다르게 보이고, 줄바꿈이 이동하며, 고객은 보관된 사본과 일치하지 않는 문서를 받게 됩니다
왜 그냥 서버에 폰트를 설치하지 않을까?
그것이 정답인 경우도 있으며, 그럴 때는 그렇게 하십시오. 하지만 흔한 세 가지 상황에서는 통하지 않습니다. 라이선스가 자동화된 렌더링을 위해 서버에 폰트를 설치하는 것을 금지할 수 있습니다. 컨테이너 이미지는 자주 다시 빌드되므로 수작업으로 설치한 폰트는 다음 배포와 함께 사라집니다. 그리고 규제를 받는 워크플로는 렌더링 스택이 버전 관리 아래 있는 아티팩트로부터 재현 가능해야 하는데, 시스템 전체에 걸친 폰트 설치는 그렇지 않습니다
프로바이더는 이 결정을 여러분의 애플리케이션 안으로 옮김으로써 세 가지 모두를 해결합니다. 폰트는 여러분이 관리하는 리소스로 배포되고, 매핑 정책은 검토할 수 있는 코드가 되며, 무엇이 우연히 설치되어 있느냐에 아무것도 의존하지 않으므로 같은 바이너리가 어디서든 동일하게 렌더링됩니다
프로바이더 설치하기
설정은 반드시 라이브러리가 로드되기 전에 이루어져야 합니다. PDFium은 초기화 시점에 시스템 폰트 정보 구조체를 받아들이고 이후 내주는 핸들을 계속 붙들고 있으므로, 문서가 열려 있는 동안 프로바이더를 바꾸면 PDFium이 여전히 붙들고 있는 폰트 핸들이 무효화됩니다. 이 컴포넌트는 렌더링을 손상시키는 대신 이를 완전히 거부합니다:
uses
PDFium;
type
TAppFontProvider = class(TInterfacedObject, IPdfSystemFontProvider)
public
function ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
end;
function TAppFontProvider.ResolveFont(const Request: TPdfSystemFontRequest;
out Font: TPdfSystemFontData): Boolean;
var
Path: string;
begin
// Deterministic mapping: face name plus weight and italic decide
// which file we ship for this request
Path := MapFaceToBundledFile(Request.FaceName, Request.Weight,
Request.Italic, Request.Charset);
Result := Path <> '';
if not Result then
Exit;
Font.FaceName := Request.FaceName;
Font.FontData := LoadFileBytes(Path); // complete sfnt or TTC bytes
Font.Charset := Request.Charset;
Font.TTCIndex := 0; // index inside a collection
end;
var
Policy: TPdfSystemFontPolicy;
begin
Policy := TPdfSystemFontPolicy.Default;
Policy.AllowDefaultFallback := False; // host decides everything
Policy.AllowFaceSubstitution := False; // reject a different face name
Policy.MaxFontBytes := 32 * 1024 * 1024;
Policy.MaxCacheEntries := 64;
ConfigureSystemFontProvider(TAppFontProvider.Create, Policy);
// Only now load the library and open documents
end;
해제는 반대 순서로 이루어집니다. 프로바이더를 PDFium에서 먼저 분리한 다음 라이브러리를 언로드합니다. 분리를 건너뛰면 네이티브 폰트 핸들이 곧 해제될 Pascal 객체를 계속 가리키게 되는데, 이는 참조 카운트 인터페이스와 C 라이브러리를 함께 쓰는 코드에서 흔히 보는 종료 시 액세스 위반입니다
정책 플래그는 실제로 무엇을 결정할까?
AllowDefaultFallback은 두 가지 운영 모드를 가르는 스위치입니다. 이를 끄면 프로바이더가 거부한 요청은 그냥 실패하는데, 이는 코퍼스 안의 모든 폰트가 확실히 파악되고 있음을 증명하려는 동안 원하는 동작입니다. 빈틈이 있으면 감춰지는 대신 즉시 드러나기 때문입니다. 이를 켜면 해결되지 않은 요청은 FPDF_GetDefaultSystemFontInfo가 반환하는 매퍼로 위임되며, 바깥세상은 여전히 페이스 이름, 문자셋, 테이블 데이터, 폰트 삭제 루틴이 출처에 따라 올바르게 라우팅되는 하나의 균일한 핸들 래퍼만 보게 됩니다
AllowFaceSubstitution은 프로바이더가 요청받은 것과 다른 페이스 이름으로 응답할 수 있는지를 결정합니다. 이를 끄면 대체가 우연이 아니라 명시적인 결정이 되며, 문서가 지정한 폰트의 메트릭이 페이지 나눔을 바꿀 만큼 다를 때 이는 중요해집니다
이 컴포넌트는 프로바이더의 모든 응답이 PDFium에 도달하기 전에 검증합니다. 빈 데이터는 거부되고, 크기가 너무 큰 폰트는 MaxFontBytes에 따라 거부되며, TTC 인덱스는 검사되고, PDFium이 테이블을 요청하면 폰트 디렉터리에서 개별 sfnt 테이블이 제공됩니다. 마지막 기능 덕분에 프로바이더는 완전한 폰트 파일 하나만 넘기고 테이블 수준 질의에 대한 응답은 이 컴포넌트가 대신 처리하게 할 수 있어, 원시 Pascal 객체를 C ABI 너머로 노출하지 않아도 됩니다
매달린 폰트 데이터 없이 캐싱하기
폰트 매핑 요청은 렌더링 도중 끊임없이 반복되므로, 응답은 모든 폰트 선택 매개변수를 포괄하는 키로 캐시되며 제한된 LRU 순서로 축출됩니다. 미묘한 부분은 수명입니다. PDFium은 방금 캐시 항목이 축출된 폰트의 바이트를 여전히 읽고 있을 수 있습니다
이 캐시는 참조 카운트된 동적 배열을 저장하고 각 네이티브 핸들은 자신만의 스냅샷을 갖고 있으므로, 축출은 사용 중인 메모리를 해제하는 대신 참조 하나를 줄일 뿐입니다. 삭제 콜백은 핸들을 해제하고 활성 카운트를 유지합니다. 실무적으로 이는 MaxCacheEntries를 진행 중인 렌더링 아래에서 데이터를 뽑아낼 위험 없이 메모리에 맞춰 조정할 수 있다는 뜻입니다
프로바이더는 내 스레드에서 호출될까?
반드시 그렇지는 않습니다. PDFium은 자신의 워커 스레드에서 매퍼를 호출할 수 있으므로, 구현은 스레드 안전해야 합니다. 공유 카운터, 캐시, 설정 관찰은 각각 이 컴포넌트 내부에서 자신만의 크리티컬 섹션으로 보호되지만, ResolveFont 안의 코드는 여러분이 직접 안전하게 만들어야 합니다
가장 안전한 형태는 변경 가능한 공유 상태를 전혀 건드리지 않는 프로바이더입니다. 시작 시점에 만든 테이블에서 읽고, 파일이나 리소스에서 바이트를 로드하고, 반환하십시오. 여러분만의 공유 캐시를 조회해야 한다면 보호하십시오. 그리고 예외는 여러분의 구현 안에 붙잡아 두십시오. Pascal 예외가 PDFium 스택을 거슬러 올라가는 일은 절대 없어야 합니다. 이 컴포넌트는 C ABI 경계에서 잡아 실패나 선택적 기본 폴백으로 변환하지만, 이를 정상적인 제어 흐름으로 의존하면 성능이 떨어지고 버그가 가려집니다. 이 컴포넌트의 나머지 부분에 적용되는 스레딩 규칙은 렌더 락 규율과 같은 원칙을 따릅니다
운영 환경에서 매핑을 증명하기
통계는 폰트 대체를 추측이 아니라 단언할 수 있는 것으로 바꿔줍니다. GetSystemFontProviderStatistics는 프로바이더가 설정되고 설치되었는지, 매핑 요청이 몇 건 있었는지, 그리고 그 요청이 캐시 히트, 프로바이더 히트, 기본 폴백 히트로 어떻게 나뉘어 처리되었는지, 그리고 거부된 응답, 실패한 요청, 살아 있는 핸들, 캐시된 폰트 개수를 함께 보고합니다:
var
Stats: TPdfSystemFontStatistics;
begin
Stats := GetSystemFontProviderStatistics;
Writeln(Format('requests=%d cache=%d provider=%d fallback=%d',
[Stats.MapRequests, Stats.CacheHits, Stats.ProviderHits,
Stats.DefaultFallbackHits]));
Writeln(Format('rejected=%d failed=%d handles=%d cached=%d',
[Stats.RejectedProviderResponses, Stats.FailedRequests,
Stats.ActiveHandles, Stats.CachedFonts]));
// In a conformance run with fallback disabled, any fallback hit or
// failed request means a document referenced a font we do not ship
if (Stats.DefaultFallbackHits > 0) or (Stats.FailedRequests > 0) then
raise Exception.Create('unmapped font encountered - update the font set');
end;
RejectedProviderResponses 개수가 늘어나는 것은 프로바이더가 정책이 거부하는 데이터, 대개는 크기가 너무 크거나 대체된 페이스로 응답하고 있다는 신호이며, 이런 요청은 조용히 폴백이나 실패로 성능이 떨어지므로 경고를 걸어둘 가치가 있습니다. 매핑 테이블을 만들기 전에 문서가 실제로 어떤 폰트를 필요로 하는지 진단하려면, PDF 폰트 속성 분석하기의 검사 경로가 문서별로 임베드된 폰트와 임베드되지 않은 폰트를 나열해 줍니다
폰트 프로비저닝, 렌더링, 텍스트 추출은 Delphi, C++Builder, Lazarus에서 같은 라이브러리 인스턴스를 공유합니다. 배포 세부 사항은 Delphi용 PDFium Component 페이지에서 설명합니다