Embed and subset a TrueType font
At a glance
Section titled “At a glance”Register a TrueType font, render text with it, and let the writer embed only the needed subset. This recipe follows the same content path as examples/04-text-and-fonts.php, with a registered TrueType (.ttf) font added.
Install
Section titled “Install”composer require nextpdf/core:^3This constraint installs the nextpdf/core package. The example runs on PHP 8.4, and the bundled LiberationSans-Regular.ttf test fixture keeps the recipe self-contained.
Conceptual overview
Section titled “Conceptual overview”Register a face with FontRegistry::register($path, $alias). The registry parses the file and returns a FontInfo, using TrueTypeParser for .ttf and .otf files. To activate the face, select its alias with setFont($alias, ...). This call also records the codepoints used.
Subsetting runs automatically at save(). The PDF font writer collects the used codepoints and calls FontSubsetter::subset(), or CffSubsetter for Compact Font Format (CFF) or OpenType faces. When the subset is smaller than the full program, the writer embeds the subset. It also rewrites BaseFont and FontName with a six-uppercase-letter, plus-joined subset tag. This is the ABCDEF+FontName form that ISO 32000-2 requires for a font subset. The writer stores the embedded TrueType program as FontFile2 in the font descriptor (ISO 32000-2).
The subset prefix is generated deterministically from the PostScript name, so a deterministic build produces a stable tag. That is why this recipe’s reproducibility profile is structural. The structural profile normalizes the subset prefix and the trailer /ID instead of asserting them byte-for-byte.
API surface
Section titled “API surface”FontRegistry::register(string $fontFile, string $alias = '', int $fontIndex = 0): FontInfo—NextPDF\Typography\FontRegistry.setFont(string $family, string $style = '', float $size = 12.0): static—NextPDF\Core\Concerns\HasTypography; pass the registered alias as$family.- Subsetting is internal to the writer (
NextPDF\Writer\PdfFontWriter->NextPDF\Typography\FontSubsetter). There is no public on or off switch: the writer always subsets when the codepoints are known and the subset is smaller.
The full PHPDoc table is generated from the source.
Code sample — Quick start
Section titled “Code sample — Quick start”<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\Document;use NextPDF\Typography\FontRegistry;
$registry = new FontRegistry();$registry->register(__DIR__ . '/MyFont-Regular.ttf', alias: 'MyFont');
$doc = Document::createStandalone();$doc->addPage();$doc->setFont('MyFont', '', 14);$doc->cell(0, 10, 'Rendered with an embedded, subset TrueType face.', newLine: true);
$doc->save(__DIR__ . '/out.pdf');Document::createStandalone() constructs its own registry. To use a registry you populated yourself, build the document through DocumentFactory, as shown in the production sample. The factory makes the writer read your registered face.
Code sample — Production
Section titled “Code sample — Production”This sample is self-contained and harness-runnable. It registers the bundled LiberationSans-Regular.ttf and renders through DocumentFactory, so the populated registry is the one in use. It relies on automatic subsetting at save.
<?php
declare(strict_types=1);
require_once __DIR__ . '/vendor/autoload.php';
use NextPDF\Core\DocumentFactory;use NextPDF\Graphics\ImageRegistry;use NextPDF\Typography\FontRegistry;
// A bundled TrueType test fixture keeps this recipe self-contained.// Replace with a font you have the right to embed.$fontPath = __DIR__ . '/../../fonts/test-fixtures/LiberationSans/LiberationSans-Regular.ttf';if (!is_file($fontPath)) { // Fall back to the repository-relative fixture location. $fontPath = dirname(__DIR__, 2) . '/fonts/test-fixtures/LiberationSans/LiberationSans-Regular.ttf';}
$fontRegistry = new FontRegistry();$fontRegistry->register($fontPath, alias: 'LiberationSans');
$imageRegistry = new ImageRegistry(maxCacheBytes: 0);$documentFactory = new DocumentFactory($fontRegistry, $imageRegistry);
$doc = $documentFactory->create();$doc->setTitle('Embedded Subset Font');$doc->addPage();
$doc->setFont('LiberationSans', '', 20);$doc->cell(0, 14, 'Embedded TrueType face', newLine: true);
$doc->setFont('LiberationSans', '', 12);$doc->multiCell(0, 7, 'Only the glyphs used by this document are embedded. ' . 'The writer subsets the font program and rewrites the BaseFont with a ' . 'deterministic six-letter subset prefix, for example ABCDEF+LiberationSans.');
$out = getenv('NEXTPDF_COOKBOOK_OUTPUT');$doc->save($out !== false ? $out : __DIR__ . '/embed-and-subset-fonts.pdf');
echo "Wrote embed-and-subset-fonts.pdf\n";Expected STDOUT:
Wrote embed-and-subset-fonts.pdfTo confirm the subset, open the output and inspect the font dictionary. The BaseFont reads <TAG>+LiberationSans, and the descriptor carries a FontFile2. Running qpdf --check reports no structural errors.
Edge cases & gotchas
Section titled “Edge cases & gotchas”- Registry ownership.
Document::createStandalone()builds its own registry. A font you registered on a separateFontRegistryis not visible to it. UseDocumentFactoryto pass your registry, as the production sample does. - Embedding rights. Subsetting does not change licensing. Embed only fonts you are licensed to embed. Some fonts set embedding-restriction bits; the parser reads those bits, but you remain responsible for compliance.
- CFF/OpenType path.
.otfand CFF faces are subset byCffSubsetter, notFontSubsetter. The behavior and subset-tag rewrite are equivalent. Only the code path differs. - No size win. Sometimes the subset is not smaller than the original. This can happen with tiny fonts or when all glyphs are used. In that case, the writer embeds the original program without a subset tag. This is correct, not a failure.
- CJK fonts. Large Chinese, Japanese, and Korean (CJK) faces use a tiered subsetting strategy per ADR-008, with an isolated subprocess and a PHP-native fallback. See Set CJK text with cmap-aware encoding for CJK specifics and current pipeline status.
Performance
Section titled “Performance”Parsing takes one pass over the font tables, and subsetting cost grows with glyph count. Non-CJK Latin faces subset in process within the wall_ms: 1500, peak_mb: 96 budget. Large CJK faces are routed to an isolated subprocess with a two-second wall-clock timeout and a PHP-native fallback (ADR-008). This routing means a slow or crashing subset cannot block the caller.
Security notes
Section titled “Security notes”A font file is untrusted binary input. The parser rejects stream-wrapper paths and null bytes. The CJK subsetting subprocess runs with no inherited database connections, file handles, or framework state. It falls back safely on crash or timeout (ADR-008). Validate the provenance of fonts accepted from end users.
Conformance
Section titled “Conformance”| Statement | Spec | Clause | reference_id |
|---|---|---|---|
| A font subset’s BaseFont/FontName carries a six-uppercase-letter, plus-joined subset prefix. | ISO 32000-2 | iso32000_2_sec9#x1.x66.p2 | |
| An embedded TrueType font program is stored as FontFile2 in the font descriptor. | ISO 32000-2 | iso32000_2_sec9#x1.x65.p15 |
This recipe shows how NextPDF embeds and subsets a TrueType face and emits a conforming subset prefix. It does not assert font-license compliance. Embedding rights are the integrator’s responsibility.
Commercial context
Section titled “Commercial context”Not applicable.