콘텐츠로 이동
getnextpdf.com

프로덕션에서 글꼴 프로비저닝하기

PDF가 노트북에서는 올바르게 렌더링되다가 컨테이너로 배포되면 빈 상자가 줄지어 나오거나 — “두부(tofu)” 글리프 — 악센트와 비라틴 문자가 누락되어 나옵니다. 원인은 거의 항상 동일합니다. 선택한 글꼴이 배포된 이미지에 존재하지 않는 것입니다.

네이티브 인프로세스 NextPDF 엔진은 글꼴 레지스트리가 읽을 수 있는 글꼴 파일에서 글꼴을 해석합니다. OS나 fontconfig 글꼴을 자동으로 발견하지 않습니다 — OS에 설치된 글꼴 파일은 그 파일을 명시적으로 등록하거나 그것을 담은 디렉터리를 FontRegistry 검색 경로에 추가할 때만 도움이 됩니다. 슬림한 베이스 이미지로 빌드된 컨테이너에는 apt/apk로 설치된 글꼴이 없으며, 있더라도 레지스트리를 그 파일로 가리키지 않으면 네이티브 엔진은 그것을 무시합니다. 해결책은 실제 글꼴 파일을 애플리케이션이나 이미지 안에 번들하고 엔진에 등록하는 것입니다. 레지스트리는 TrueType(.ttf), OpenType(.otf), TrueType Collection(.ttc) 파일을 읽습니다. 레거시 Type1(.pfb)도 허용되지만 새 작업에는 거의 필요하지 않습니다.

시작하기 전에 다음 요소가 준비되었는지 확인하십시오.

  • NextPDF 코어가 설치되어 있습니다.
  • 사용하려는 실제 글꼴 파일이 있고, 그것을 임베드할 라이선스가 있습니다. 임베드 권리는 여러분의 책임입니다 — TrueType 글꼴 임베드 및 서브셋을 참고하십시오.
  • 빌드가 그 파일을 배포 산출물로 복사할 수 있습니다.

이것은 운영 방법 안내입니다. 코드는 최소한이며, 작업은 빌드와 파일 시스템 레이아웃에 있습니다. 단일 페이스를 등록하고 서브셋하는 API 수준 메커니즘은 위에 연결된 임베드-및-서브셋 레시피를 읽으십시오. 이 페이지는 파일을 박스에 올리고 엔진이 그것을 가리키게 하는 것을 다룹니다.

네이티브 엔진이 OS 글꼴을 자동으로 찾지 않는 이유

섹션 제목: “네이티브 엔진이 OS 글꼴을 자동으로 찾지 않는 이유”

두 개의 구별되는 렌더링 경로가 있으며, 글꼴 이야기는 그 둘 사이에서 다릅니다.

  • 네이티브 인프로세스 엔진(기본값, Document / writeHtml): 엔진은 발견을 위해 운영 체제의 글꼴 시스템이나 fontconfig를 호출하지 않습니다. 글꼴 레지스트리를 통해 페이스를 해석하며, 이는 등록한 특정 글꼴 파일을 읽거나 검색 경로로 구성한 디렉터리 안에서 하나를 찾습니다. apt-get install fonts-noto로 글꼴을 설치하거나 fc-cache를 실행하는 것은 그 자체로는 아무것도 하지 않습니다 — 네이티브 엔진은 그 파일을 등록하거나 그 디렉터리를 레지스트리의 검색 경로에 추가할 때만 봅니다.
  • Chrome 브리지(헤드리스 브라우저를 구동하는 HTML-to-PDF 렌더러): 이 경로는 브라우저의 일반 글꼴 발견을 통해 호스트에 설치된 글꼴을 실제로 사용하므로, 거기서는 apt/apk 글꼴 패키지와 fontconfig가 중요합니다.

“Dockerfile에 이 시스템 글꼴 패키지를 설치하라”는 일반적인 안내를 읽었다면, 그것은 Chrome 브리지에 적용되며 이 페이지가 다루는 네이티브 엔진에는 적용되지 않습니다. 네이티브 생성의 경우 파일을 번들하고 등록하십시오.

1단계 — 실제 글꼴 파일 번들하기

섹션 제목: “1단계 — 실제 글꼴 파일 번들하기”

글꼴 파일이 버전 관리되고 모든 빌드와 함께 배포되도록 애플리케이션 트리 안에 넣으십시오. 관례적인 위치는 resources/fonts/ 디렉터리입니다.

your-app/
├── resources/
│ └── fonts/
│ ├── DejaVuSans.ttf
│ ├── DejaVuSans-B.ttf
│ └── NotoSansCJK-Regular.ttc
└── src/

엔진의 디렉터리 검색이 패밀리와 스타일로 찾을 수 있도록 파일 이름을 지으십시오. 특정 파일이 아니라 디렉터리를 등록하고 나중에 setFont('DejaVuSans', 'B', 12)를 호출하면, 엔진은 각 구성된 디렉터리에서 DejaVuSans-B.ttf, DejaVuSansB.ttf, 또는 DejaVuSans.ttf 같은 파일을 찾습니다. 디렉터리 검색은 그 후보 이름을 setFont에 전달하는 동일한 단일 글자 스타일 코드(B는 볼드, I는 이탤릭, BI는 볼드 이탤릭)에서 구성하며, 철자로 풀어쓴 단어가 아닙니다 — 따라서 신뢰할 수 있는 형태는 Family-Bold.ttf아니라 Family-<StyleCode>.ttf(예: DejaVuSans-B.ttf 또는 DejaVuSans-BI.ttf)입니다. DejaVuSans-Bold.ttf라는 이름의 파일은 디렉터리 검색으로는 결코 찾을 수 없습니다. 그런 파일을 사용하려면 register()로 명시적으로 등록하십시오 — 이는 글꼴을 파싱하여 파일 자체의 name 테이블에서 읽은 패밀리와 스타일로 색인하므로, 철자로 풀어쓴 파일 이름은 더 이상 중요하지 않습니다(2단계 참고).

파일을 보이게 하는 두 가지 동등한 방법이 있습니다. 둘 다 NextPDF\Typography\FontRegistry를 거치며, 이는 NextPDF\Contracts\FontRegistryInterface를 구현합니다.

정확한 페이스를 제어할 때 특정 파일을 별칭 아래에 등록하십시오.

use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();
$registry->register(__DIR__ . '/../resources/fonts/DejaVuSans.ttf', alias: 'DejaVuSans');

register(string $fontFile, string $alias = '', int $fontIndex = 0).ttf, .otf, .ttc 파일과 레거시 Type1 .pfb(동일한 경로에서 그 동반 .afm 메트릭을 로드함)를 받습니다. $fontIndex는 TrueType Collection(.ttc) 안의 하위 글꼴을 선택합니다. register()는 파일을 파싱하고 그 자체의 name 테이블에서 읽은 패밀리와 스타일로 페이스를 색인하므로, 일단 등록되면 물리적 파일 이름은 무관합니다. 선택적 $alias는 페이스에 대한 추가 조회 이름일 뿐입니다 — 스타일 코드가 아니며 파일이 제공하는 스타일을 바꾸지 않습니다. 글꼴의 임베드된 패밀리 이름과 다른 이름으로 setFont()를 호출하고 싶을 때 전달하십시오. 파싱된 FontInfo를 반환합니다.

엔진이 여러분이 제어하는 폴더에서 이름으로 페이스를 해석하기를 원할 때 디렉터리를 등록하십시오.

$registry = new FontRegistry('/var/www/app/resources/fonts');
// or, equivalently, after construction:
$registry->addFontDirectory('/var/www/app/resources/fonts');

FontRegistry 생성자는 그 디렉터리를 첫 번째 인수로 받으며, addFontDirectory()는 검색 경로를 더 추가합니다. 베어 Document도 독립형 케이스를 위해 addFontDirectory()를 노출합니다.

직접 채운 레지스트리를 사용하려면, 그 정확한 레지스트리를 생성하는 모든 문서에 연결하는 DocumentFactory를 통해 문서를 구축하십시오.

use NextPDF\Core\DocumentFactory;
use NextPDF\Graphics\ImageRegistry;
$factory = new DocumentFactory($registry, new ImageRegistry(maxCacheBytes: 0));
$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'Réndéred wîth a bundled face — no tofu.', newLine: true);
$doc->save('/tmp/out.pdf');

Document::createStandalone()자체 내부 레지스트리를 구축하므로, 별도의 FontRegistry에 등록한 페이스는 그것에게 보이지 않습니다. 프로덕션에서는 DocumentFactory(또는 프레임워크의 팩토리)를 통해서 진행하여 채워진 레지스트리가 사용 중인 것이 되도록 하십시오.

각 프레임워크 통합은 동일한 두 개념을 구성으로 노출하므로, 레지스트리를 직접 건드릴 일은 드뭅니다. Laravel 패키지의 nextpdf.php에서 fonts_path(기본값 NEXTPDF_FONTS_PATH, 그것이 없으면 resource_path('fonts')로 폴백)는 검색 디렉터리이고, preload_fonts는 워커 부팅 시 파싱되는 절대 글꼴 파일 경로 목록입니다. fonts_path를 번들한 디렉터리로 가리키면 등록된 페이스가 자동으로 해석됩니다.

3단계 — Docker 이미지에서 글꼴 프로비저닝하기

섹션 제목: “3단계 — Docker 이미지에서 글꼴 프로비저닝하기”

컨테이너에서 글꼴 파일은 빌드 시점에 복사되어 이미지 레이어의 일부여야 합니다. resources/fonts/ 아래에 번들하면 애플리케이션 코드와 글꼴이 함께 배포되므로, 일반적인 COPY . .가 이미 그것들을 운반합니다. 글꼴을 빌드 컨텍스트 밖에 둔다면 명시적으로 복사하고, 등록하는 경로가 이미지 안의 경로와 일치하는지 확인하십시오.

# Native engine: NO system font packages are required.
# The native engine does not discover OS-installed fonts automatically; install OS
# font packages (`apt-get install fonts-*`) only if you also register them or point
# the font registry's search directory at their files.
FROM php:8.4-cli
WORKDIR /var/www/app
# Bundle the application, including resources/fonts/, into the image.
COPY . /var/www/app
# Make the bundled directory the engine's font search path.
ENV NEXTPDF_FONTS_PATH=/var/www/app/resources/fonts
CMD ["php", "bin/generate.php"]

불변 또는 읽기 전용 파일 시스템(readOnlyRootFilesystem 컨테이너, 서버리스 이미지, 또는 강화된 호스트)에서는 글꼴 파일이 생성 시점에 읽히기만 하고 결코 기록되지 않으므로, 읽기 전용 마운트로 괜찮습니다. 엔진이 원할 수 있는 유일한 쓰기는 그 파싱된 글꼴 캐시입니다. 그 디렉터리에 작은 쓰기 가능한 볼륨을 주거나, 부팅 시 레지스트리를 워밍업하고 잠그어(다음 섹션) 런타임 쓰기나 등록이 시도되지 않도록 하십시오.

장기 실행 워커에서는 부팅 시 모든 페이스를 한 번 파싱한 뒤, 요청별 등록이 일어나지 않고 잘못된 구성이 조용히 폴백하는 대신 큰 소리로 실패하도록 레지스트리를 잠그십시오.

$registry = new FontRegistry('/var/www/app/resources/fonts');
$registry->warmup([
'/var/www/app/resources/fonts/DejaVuSans.ttf',
'/var/www/app/resources/fonts/DejaVuSans-B.ttf',
]);
$registry->lock();

lock() 후에는 register(), addFontDirectory(), warmup()이 던지며, 이는 “이미지 안의 잘못된 경로” 실수를 프로덕션에서의 두부 페이지가 아니라 하드 부팅 실패로 바꿉니다.

필요한 각 페이스로 한 페이지를 렌더링하는 배포 스모크 검사를 추가하십시오. 아래 헤더 검사는 문서가 출력을 생성했는지만 확인합니다 — 글꼴이 파싱되었거나, 임베드되었거나, 심지어 해석되었음을 증명하지 않습니다. 엔진이 찾을 수 없는 페이스는 표준 베이스 글꼴로 폴백될 수 있고(그리고 현재의 비엄격 동작 하에서는 적합성 프로필이 대신 번들된 대체물을 공급할 수도 있음), 그러면서도 유효하고 비어 있지 않은 PDF를 여전히 방출할 수 있습니다 — 따라서 그 폴백이 일어나는 경우에도 이 검사만으로는 조용한 성능 저하를 잡지 못합니다. 폴백이 모든 경로에서 보장되거나 조용하다고 의존하지 마십시오. 아래에 표시된 대로 임베드된 프로그램을 직접 확인하십시오.

$doc = $factory->create();
$doc->addPage();
$doc->setFont('DejaVuSans', '', 12);
$doc->cell(0, 10, 'warmup check', newLine: true);
$pdf = $doc->getPdfData();
// `getPdfData()` would normally throw on a real failure; this header check only
// confirms serialization returned PDF bytes, not that any specific font resolved.
if (!str_starts_with($pdf, '%PDF')) {
throw new RuntimeException('Font warmup smoke check produced no PDF output.');
}

페이스가 누락되었을 때 실제로 배포를 실패시키려면, 방출된 PDF에서 임베드된 글꼴 프로그램을 확인하십시오. 해석되는 등록된 페이스는 임베드된 프로그램을 가진 자체 글꼴 딕셔너리를 담으므로, 그 존재를 단언하면 요청한 페이스가 결코 해석되지 않은(엔진이 무엇으로 폴백했든) 경우를 잡아냅니다. 이는 헤더 검사가 놓치는 경우입니다. 어떤 키가 프로그램을 담는지는 아웃라인 형식에 따라 다릅니다. TrueType 아웃라인(.ttf, .ttc)은 /FontFile2를, CFF/OpenType 아웃라인(PostScript 아웃라인을 가진 .otf)은 /FontFile3을, 레거시 Type1(.pfb)은 /FontFile을 사용합니다.

형식 불문 “어떤 글꼴 프로그램이 임베드됨” 신호만 필요하다면, /FontFile 단독으로 테스트하십시오 — /FontFile/FontFile2/FontFile3 둘 다의 부분 문자열이므로, 베어 부분 문자열 검사가 이미 모든 아웃라인 유형에 매칭되며 /FontFile2//FontFile3을 추가 || 분기로 더하는 것은 중복입니다.

if (!str_contains($pdf, '/FontFile')) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

다만 베어 /FontFile 부분 문자열은 아웃라인 유형을 구별할 수 없습니다. 구별하려면 /FontFile/FontFile2/FontFile3에도 발동하지 않도록 단어 경계로 정확한 토큰에 매칭하십시오.

$isTrueType = preg_match('~/FontFile2\b~', $pdf) === 1; // TrueType (.ttf/.ttc)
$isCffOtf = preg_match('~/FontFile3\b~', $pdf) === 1; // CFF/OpenType (.otf)
$isType1 = preg_match('~/FontFile(?![23])\b~', $pdf) === 1; // Type1 (.pfb)
if (!$isTrueType && !$isCffOtf && !$isType1) {
throw new RuntimeException('No embedded font program found — face fell back.');
}

어느 쪽이든 이를 신뢰할 수 있는 배포 게이트가 아니라 거친 휴리스틱일 뿐으로 취급하십시오. 직렬화된 PDF에 대한 원시 바이트 검색은 여러 이유로 부정확합니다. 글꼴 프로그램이 압축된 객체 스트림 안에 있을 수 있고(거기서는 /FontFile*이 평문 바이트로 결코 나타나지 않음), 증분 업데이트가 객체를 추가하거나 대체할 수 있으며, 비임베드 또는 standard-14 글꼴은 정당하게 어떤 글꼴 프로그램도 담지 않고, 직렬화 차이(객체 순서, 공백, 이름 인코딩)가 토큰을 옮기거나 숨길 수 있습니다. 잘해야 어떤 페이스가 프로그램을 임베드했음을 확인할 뿐, 원했던 특정 페이스가 해석되었음은 결코 아닙니다.

실제 배포 게이트의 경우 바이트 검색에 의존하지 마십시오. 방출된 PDF를 제대로 된 PDF 파서나 객체 검사기로 파싱하고, 대상 페이스의 글꼴 객체가 임베드된 /FontFile//FontFile2//FontFile3 프로그램을 담고 있음을 단언하거나, 통합에서 사용할 수 있다면 제품이 제공하는 글꼴 해석 단언을 사용하십시오. 위의 토큰 인식 정규식은 빠른 로컬 정상성 검사에 유용하지만, 배포를 실패시켜야 하는 것은 구조적 검사입니다. 임베딩과 글꼴 딕셔너리 구조는 TrueType 글꼴 임베드 및 서브셋에 설명되어 있습니다.

  • createStandalone()은 자체 레지스트리를 가집니다. 별도의 FontRegistry에 등록된 페이스는 독립형 문서에게 보이지 않습니다. DocumentFactory(또는 프레임워크 팩토리)를 사용하여 여러분의 레지스트리가 활성인 것이 되도록 하십시오.
  • 스타일 파일은 파일로 존재해야 합니다. 엔진은 레귤러 페이스에서 볼드나 이탤릭을 합성하지 않습니다. setFont('DejaVuSans', 'B')를 호출하면 디렉터리 검색은 DejaVuSans-B.ttf, DejaVuSansB.ttf, 또는 DejaVuSans.ttf(소문자와 .otf 변형도)를 찾습니다 — 후보를 리터럴 B 스타일 코드에서 구성하므로 DejaVuSans-Bold.ttf는 결코 찾지 않습니다. DejaVuSans-Bold.ttf 같은 철자로 풀어쓴 이름의 파일은 register()로 명시적으로 등록할 때만 해석되며, 이는 파일 이름과 무관하게 파일 자체의 name 테이블에서 읽은 패밀리와 스타일로 색인합니다. 디렉터리 검색이 그것을 찾도록 의존하면 미스가 발생하고, 그 후 엔진이 베이스 글꼴로 폴백할 수 있습니다(보장되거나 항상 조용한 경로가 아님) — 이 페이지가 경고하는 성능 저하입니다.
  • 스트림 래퍼 및 원격 경로는 거부됩니다. 레지스트리는 URI 스킴이나 null 바이트를 포함하는 경로를 거부합니다. 로컬 파일만 등록하십시오. 런타임에 가져오는 글꼴은 원시 바이트와 함께 registerFromBinary()를 사용하십시오.
  • 잠긴 레지스트리는 불변입니다. lock()을 호출하면, 이후의 어떤 register(), addFontDirectory(), warmup()도 던집니다. 조회 메서드는 계속 사용 가능합니다. 잠그기 전에 모든 것을 등록하고 워밍업하십시오.
  • CJK 컬렉션은 큽니다. .ttc의 올바른 하위 글꼴을 $fontIndex로 등록하고, 더 큰 임베드 서브셋을 위한 예산을 잡으십시오. 임베드-및-서브셋 레시피의 CJK 참고 사항을 보십시오.
  • 글꼴 파일은 신뢰할 수 없는 바이너리 입력입니다. 신뢰하는 출처의 글꼴만 번들하고, 최종 사용자로부터 받은 어떤 페이스든 출처를 검증하십시오.
  • 워밍업 후 레지스트리를 잠그면 런타임 변형 표면이 제거되고, 경로 실수가 출력을 조용히 저하시키는 대신 부팅 시 실패하게 됩니다.
  • 사용자 입력을 등록된 파일 경로에 보간하지 마십시오. 번들된 페이스의 고정 세트를 등록하고, 요청이 임의의 파일 시스템 경로를 선택하도록 두지 마십시오.

이 안내는 규범적 표준 주장을 하지 않습니다. 표시된 모든 심볼은 검증된 공개 표면입니다. NextPDF\Typography\FontRegistry(register(), addFontDirectory(), warmup(), lock(), 디렉터리 생성자 인수), 그 NextPDF\Contracts\FontRegistryInterface 계약, NextPDF\Core\DocumentFactory::create(), 그리고 NextPDF\Core\Document::setFont() / addFontDirectory()입니다. Laravel의 fonts_pathpreload_fonts 키는 nextpdf/laravel 패키지의 문서화된 구성입니다. 임베딩과 서브셋 태그 동작은, 그 ISO 32000-2 인용과 함께, 참고 자료 아래 연결된 임베드-및-서브셋 레시피에 문서화되어 있습니다.