콘텐츠로 이동

타이포그래피: 글꼴 레지스트리, 서브셋, CMap, 인코딩, BiDi

타이포그래피 모듈은 글꼴 파일과 유니코드 문자열을 PDF 콘텐츠 스트림에 필요한 바이트로 변환합니다. 이 모듈은 글꼴 구문 분석, 프로세스 수명 주기 레지스트리, 글리프 서브셋, ToUnicode CMap, cmap 인식 인코딩 전략, 유니코드 양방향 엔진을 담당합니다.

Terminal window
composer require nextpdf/core:^3

FontRegistry는 프로세스 수명 동안 유지되는 글꼴 저장소이며 FontRegistryInterface를 구현합니다. TrueType, OpenType, TTC 또는 Type 1(PFB 및 AFM) 파일을 한 번 구문 분석하여 불변 FontInfo를 반환합니다. 레지스트리는 장시간 실행되는 워커를 위해 설계되었습니다. 부팅 시 글꼴 집합을 워밍한 뒤 lock()을 호출합니다. 조회로 트래픽을 계속 처리할 수 있지만, 레지스트리는 이후의 모든 변경을 거부합니다. 순수 PHP 데이터, 즉 구문 분석된 메타데이터와 원시 글꼴 바이트만 보관하므로 워커 풀이 하나의 인스턴스를 공유할 수 있습니다. registerFromBinary()는 원시 글꼴 바이트를 받으며, HTML @font-face 브리지는 원격 소스나 데이터 URI에서 가져온 글꼴에 이를 사용합니다.

엔진은 사용하는 모든 글꼴을 임베드하고 서브셋합니다. 임베드된 글꼴 프로그램은 PDF 내부에 포함되어 함께 이동하므로 문서는 설치된 시스템 글꼴에 의존하지 않고 모든 뷰어에서 동일하게 렌더링됩니다 — ISO 32000-2 §9. 서브셋은 문서가 참조하는 글리프만 담으며, 이는 CJK 또는 유니코드가 풍부한 콘텐츠에 결정적입니다 — ISO 32000-2 §9. FontSubsetter는 원본 테이블 디렉터리를 구문 분석하고, cmap을 추출하며, 합성 글리프 의존성을 전이적 폐쇄로 해석하고, head, hhea, maxp, cmap, loca, glyf, 그리고 hmtx 테이블을 다시 빌드합니다. 원래 글리프 식별자 번호 체계를 보존하고 사용되지 않는 슬롯을 0으로 채우므로, CIDToGIDMap/Identity 형태가 유효하게 유지됩니다. 서브셋으로 절약되는 양이 10퍼센트 미만일 경우 원본 글꼴을 변경하지 않고 반환하여, 비용을 회수하지 못하는 작업을 피합니다. CffSubsetter는 Compact Font Format 아웃라인 테이블을 갖는 OpenType 글꼴에 대해 같은 작업을 수행합니다.

텍스트 방출은 3단계 변환입니다. 유니코드 코드 포인트, 그다음 콘텐츠 스트림 안의 문자 코드, 마지막으로 글꼴 내부의 글리프 식별자 순서입니다. 모듈은 이 과정을 명시적인 협력 객체 하나로 모델링합니다. FontInfo::encodeText()가 파사드이며, FontEncodingStrategyResolver가 글꼴별로 디스패치합니다. 유니코드 cmap을 갖는 임베드된 TrueType 또는 OpenType 글꼴은 TrueTypeCmapStrategy로 라우팅되며, 이는 2바이트 Identity-H 16진수 스트림을 방출합니다. 이는 Identity-H CMap과 CIDFontType2 후손을 갖는 Type 0 글꼴이 요구하는 형태입니다(ISO 32000-2 §9.7.4; 일치하는 RAG 청크 다이제스트는 라이선스 상한에 의해 잘려서 반환되었으며 _downgraded-claims-o3.md에 기록됨). 그 밖의 모든 글꼴, 즉 Base 14 표준 글꼴과 Type 1 PFB 및 AFM은 Base14EncodingStrategy로 라우팅되며, 이는 단일 바이트 WinAnsi 리터럴 문자열을 방출합니다. 이 스트림은 전체 WinAnsiEncoding(Windows 코드 페이지 1252) 레퍼토리, 즉 악센트가 있는 라틴 문자, 유로 기호, 일반적인 조판 문장 부호를 포괄합니다. 그 범위를 벗어나는 코드 포인트는 단일 바이트 스트림에서 누락되며, 이를 포괄하는 글꼴이 등록되어 있으면 클러스터별 글꼴 폴백을 거칩니다(ISO 32000-2 Annex D.2). 리졸버는 FontInfo 값 공간 전체에 대해 전역적이며, 널을 허용하는 경로는 없습니다. ToUnicodeCMapBuilder는 리더가 Identity-H 글꼴에서 원래 유니코드를 복원할 수 있게 하는 /ToUnicode 리소스를 빌드합니다. 탐욕적 bfrange 병합과 블록당 100개 항목 상한을 적용합니다.

BidiEngine은 유니코드 양방향 알고리즘(UAX #9, Unicode 16)을 처리하는 경계 서비스입니다. 분리(isolate) 지원이 꺼져 있으면 레거시 리졸버에 위임하므로 기존 호출자는 영향을 받지 않습니다. 켜져 있으면 분리 인식 파이프라인을 실행합니다. 최대 깊이 125의 명시적 분리 스택, 약한 유형 패스, 짝 괄호 해석을 포함한 중립 유형 패스, 그리고 암시적 수준 및 줄 재정렬 패스가 여기에 포함됩니다. 후보 글꼴의 CJK 글리프 커버리지는 별도의 진단입니다. CjkFontValidator는 스크립트별로 필요한 유니코드 블록을 샘플링하여 커버리지 백분율을 보고합니다.

유형종류주요 멤버안정성도입 버전
FontRegistryfinal classregister(), registerType1(), registerFromBinary(), registerFromDirectory(), get(), has(), all(), warmup(), lock(), isLocked(), memoryUsage()안정적1.7.0
FontInfofinal readonly class$family, $type, $widths, $unicodeMap, $cmapForward, getKey(), encodeText()안정적1.0.0
FontSubsetterfinal classsubset(string, array<int>, int): string안정적1.0.0
CffSubsetterfinal classOpenType/CFF 아웃라인 서브셋안정적1.0.0
FontEncodingStrategyResolverfinal classresolve(FontInfo): FontEncodingStrategy안정적2.7.0
ToUnicodeCMapBuilderfinal classbuildFromRun(), buildFromMap(), encodeUnicodeUtf16Be()안정적2.7.0
BidiEnginefinal classUAX #9 분리 인식 처리안정적3.1.0
CjkFontValidatorfinal classvalidateCoverage(), detectScript(), isCjkCodepoint()안정적1.0.0

FontInfo는 불변입니다. 생성자 시그니처와 공개 속성이 고정되어 있습니다. 인코딩 전략은 (FontInfo, UTF-8 text)의 순수 함수입니다. 같은 입력이면 호출할 때마다 같은 EncodedGlyphRun을 반환합니다.

examples/35-cjk-cmap-demo.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Typography\Encoding\EncodingMode;
use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$cjkFont = $registry->register('/path/to/NotoSansTC-Regular.ttf', alias: 'NotoSansTC');
$encoded = $cjkFont->encodeText('PDF 2.0 引擎 — 使用 CMap 編碼');
// An embedded CJK TrueType face resolves to the two-byte Identity-H path.
assert($encoded->mode === EncodingMode::TwoByteCid);

register()는 글꼴을 한 번 구문 분석하고 불변 FontInfo를 반환합니다. encodeText()는 리졸버를 통해 라우팅됩니다. 반환값은 바이트 스트림, PDF 문자열 피연산자, 글리프별 전진 너비, 그리고 GID-유니코드 맵을 담은 EncodedGlyphRun이며, 이 맵은 /ToUnicode CMap이 사용합니다.

examples/typography/registry-warmup.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Exception\NextPdfException;
use NextPDF\Typography\FontRegistry;
use Psr\Log\LoggerInterface;
final readonly class FontBootstrap
{
public function __construct(
private FontRegistry $registry,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at worker boot, then lock the registry for the
* lifetime of the process.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->registry->warmup($fontFiles);
$this->registry->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
$report = $this->registry->memoryUsage();
$this->logger->info('Font cache primed', [
'fonts' => $report->entryCount,
'bytes' => $report->currentBytes,
]);
}
}

warmup() 다음에 lock()을 호출하는 것이 워커 부팅 순서입니다. lock() 이후에는 모든 변경이 예외를 발생시킵니다. 조회는 계속 트래픽을 처리할 수 있습니다. memoryUsage()MemoryReport를 반환하므로 워커가 예산 대비 글꼴 캐시를 추적할 수 있습니다.

  • 잠긴 레지스트리는 register(), registerFromBinary(), addFontDirectory(), 그리고 warmup()을 거부합니다. 부팅 시 워밍한 뒤 잠그고, 요청 처리 중에는 절대 등록하지 마십시오.
  • FontSubsetter::subset()은 절약량이 10퍼센트 미만이거나 필수 테이블이 누락된 경우 원본 바이트를 변경하지 않고 반환합니다. 입력과 동일하게 반환된 글꼴은 실패가 아니라 문서화된 이득이 없는 경로입니다.
  • 서브셋터는 원래 글리프 식별자 번호 체계를 보존하고 사용되지 않는 글리프를 0으로 채웁니다. 이는 CIDToGIDMap /Identity를 유효하게 유지합니다. 글리프 식별자가 연속 범위로 다시 번호가 매겨진다고 가정하지 마십시오.
  • registerFromBinary()는 바이트를 구문 분석하기 위해 임시 파일에 기록하고, 확장 파일과 tempnam() 기본 파일을 모두 finally 블록에서 삭제합니다. 신뢰할 수 없는 글꼴 데이터는 구문 분석 공격 표면입니다. 파서에 도달하기 전에 차단하십시오(보안 참고 사항 참조).
  • BidiEngine은 분리 지원이 꺼져 있으면 레거시 리졸버에 그대로 위임합니다. 그러면 분리 서식 문자는 경계 중립으로 처리되어 통과합니다. 완전한 UAX #9 동작을 위해서는 적합성 정책을 통해 분리 지원을 켜십시오.
  • CjkFontValidator는 모든 코드 포인트를 테스트하는 대신 일정 간격으로 코드 포인트를 샘플링하므로, 커버리지 수치는 완전한 집계가 아니라 통계적으로 충분한 추정치입니다.

첫 사용 시에는 글꼴 구문 분석이 비용의 대부분을 차지하며, 레지스트리는 이 비용이 프로세스당 한 번만 발생하도록 합니다. 워밍 이후 get()has()는 O(1) 맵 조회입니다. 서브셋 비용은 글꼴의 전체 글리프 테이블이 아니라 문서가 실제로 사용하는 글리프 수에 비례합니다. 따라서 서브셋은 CJK 콘텐츠에서 크기와 속도 양쪽에 이득을 줍니다. 서브셋터는 이진 검색, 사전 할당된 버퍼, 그리고 대량 문자열 연산을 통해 20,000개 이상의 글리프를 갖는 글꼴을 처리합니다. 합성 글리프 해석에는 한도가 있습니다. 순환 구성 요소 참조를 방어하기 위해 폐쇄 반복을 100회로 제한합니다. cmap Format 12 파서는 악성 글꼴에 대해 메모리를 제한하기 위해 그룹 및 항목 수에 상한을 둡니다. 실제 경과 시간 1500ms와 피크 64MB의 performance_budget은 일반적인 글꼴 워밍과 문서 렌더링을 포괄합니다.

두 가지 표면이 보안상 중요합니다. 첫 번째는 글꼴 입력입니다. register()registerFromBinary()는 임의의 바이트를 구문 분석합니다. registerFromBinary()는 임시 파일을 생성합니다. 스트림 래퍼와 경로 내 널 바이트는 경계에서 거부됩니다. 신뢰할 수 없는 글꼴 데이터는 파서에 도달하기 전에 파일 크기와 글리프 수를 제한하는 외부 리소스 정책을 통과해야 합니다. 서브셋터의 이진 리더는 모든 오프셋에 대해 경계 검사를 수행합니다. cmap 파서는 그룹, 항목, 테이블 수에 상한을 둡니다(Format 12에서 numGroups > 31000 및 항목 상한 200,000). 따라서 악의적으로 조작된 글꼴이 무제한 할당을 유발할 수 없습니다. 두 번째 표면은 텍스트 복원입니다. ToUnicodeCMapBuilder는 모든 문자 코드가 16비트 코드 공간 안에 있고 모든 유니코드 값이 유효한 스칼라인지 검증합니다. 서로게이트 반쪽은 거부됩니다. 따라서 형식이 잘못된 맵이 손상된 추출 리소스를 생성할 수 없습니다. 외부에서 제공된 모든 글꼴 또는 텍스트는 신뢰할 수 없는 것으로 취급하십시오.

주장표준조항증거
문서가 사용하는 모든 글꼴이 임베드되므로 문서는 시스템 글꼴에 의존하지 않고 렌더링됩니다.ISO 32000-2§9
임베드된 글꼴은 문서가 참조하는 글리프로 서브셋됩니다.ISO 32000-2§9
임베드된 CJK TrueType 페이스는 Identity-H CMap과 CIDFontType2 후손을 갖는 Type 0 글꼴로 방출됩니다.ISO 32000-2§9.7.4RAG 다이제스트는 라이선스 상한에 의해 잘려서 반환되었습니다. 접두사는 7a5258772f508e3b이며, 참조 위치는 _downgraded-claims-o3.md

처음 두 조항은 의역되어 다이제스트로 고정되었습니다. 세 번째 조항의 전체 RAG 다이제스트는 반환되지 않았으며(라이선스 상한 절단), ADR-013 및 cmap 인코더 개발자 개요로 입증되어 강등된 것으로 기록되었습니다. NextPDF는 규범 텍스트를 복제하지 않습니다. CJK 콘텐츠에 대한 PDF/A-4 및 PDF/UA-2 적합성은 작성자 측 서브셋과 그로부터 추적되는 /ToUnicode 연결에 의해 결정됩니다.

상용 OpenType 기능 팩과 프리미엄 글꼴 대체 체인은 Core 레지스트리와 인코딩 접점 위에 구축됩니다. Core 타이포그래피 모듈은 라이선스 없이 모든 글꼴을 임베드, 서브셋, 인코딩합니다. 유료 팩은 큐레이션된 대체 해석을 추가합니다. 전환 링크를 생략한 것은 의도적입니다. 이 문서는 영업 경로가 아니라 문서입니다.