Skip to content

Custom fonts: the FontRegistry extension contract

FontRegistryInterface defines the process-lifetime contract for registering and finding fonts. Register fonts from a file path, a directory, or raw binary data, then lock the registry so production workers cannot mutate it.

Terminal window
composer require nextpdf/core:^3

The font registry is a singleton that outlives each Document instance. It stores only pure PHP data, with no resource handles or extension objects, so you can share it across requests in a long-running worker.

Use one of three registration paths:

  • From a file. register() parses a .ttf, .otf, .ttc, or .pfb file and returns metadata. For a TrueType Collection, pass the sub-font index.
  • From a directory. addFontDirectory() adds a search path that the engine scans when it resolves a family by name.
  • From binary data. registerFromBinary() parses raw TrueType or OpenType bytes. Use this path for the @font-face bridge when fonts come from a data: Uniform Resource Identifier (URI) or a remote source.

To reduce first-request latency, call warmup() at worker boot to pre-parse a batch of fonts. Then call lock(). After lock(), every mutation method throws LogicException: register(), addFontDirectory(), warmup(), registerBase14(), and registerFromBinary(). Lookup methods stay available: get(), has(), all(), and getSearchDirectories(). This lock protects production workers by ensuring no request can change the shared font set.

In most cases, you do not implement FontRegistryInterface. The engine provides the implementation, and you call it. Implement it only when you need a custom font-resolution strategy, such as one backed by a content-addressed store. In both cases, the contract remains the boundary.

NextPDF\Contracts\FontRegistryInterface (stable, since 1.7.0):

MethodReturnsPurpose
register(string $fontFile, string $alias, int $fontIndex)FontInfoParse and register a font file. Throws on a locked registry or unparsable file.
registerFromBinary(string $fontData, string $alias)FontInfoRegister a font from raw TrueType or OpenType bytes.
registerBase14(string $key, FontInfo $font)voidRegister a prebuilt Base 14 standard font.
addFontDirectory(string $directory)voidAdd a font search directory.
warmup(array $fontFiles)voidPre-parse a batch of fonts at worker boot.
lock()voidFreeze the registry to prevent further mutation.
isLocked()boolReport whether the registry is locked.
get(string $family, string $style)FontInfo | nullLook up a font by family and style.
has(string $key)boolCheck whether a registration key exists.
all()array<string, FontInfo>Return every registered font.
getSearchDirectories()list<string>Return search directories in order.
memoryUsage()MemoryReportReport current registry memory use.
<?php
declare(strict_types=1);
use NextPDF\Contracts\FontRegistryInterface;
/** @var FontRegistryInterface $fonts */
$info = $fonts->register('/srv/fonts/Inter-Regular.ttf', 'Inter');
if (!$fonts->has('inter')) {
throw new RuntimeException('Inter failed to register');
}

At worker boot, warm the font set, lock the registry, and observe each load for license tracking. This example uses only public types.

<?php
declare(strict_types=1);
use NextPDF\Contracts\FontRegistryInterface;
use NextPDF\Event\Content\FontLoadedEvent;
use NextPDF\Event\EventDispatcher;
use NextPDF\Event\ListenerProvider;
use Psr\Log\LoggerInterface;
final class FontWarmup
{
/** @param list<string> $fontFiles */
public function __construct(
private readonly FontRegistryInterface $fonts,
private readonly LoggerInterface $logger,
private readonly array $fontFiles,
) {}
public function boot(): EventDispatcher
{
$listeners = new ListenerProvider();
$listeners->addListener(
FontLoadedEvent::class,
function (FontLoadedEvent $event): void {
$this->logger->info('font.loaded', [
'family' => $event->family,
'style' => $event->style,
'type' => $event->fontType->name,
]);
},
);
if (!$this->fonts->isLocked()) {
$this->fonts->warmup($this->fontFiles);
$this->fonts->lock();
}
return new EventDispatcher($listeners);
}
}
  • Locked registry. After lock(), any mutation throws LogicException. Check isLocked() before a conditional warmup in a recycled worker.
  • Binary registration is uncached by key. registerFromBinary() writes to a temporary file and parses it. Use the returned FontInfo as the handle.
  • TrueType Collection (TTC) index. For a TrueType Collection, the third argument to register() selects the sub-font. The default 0 selects the first face.
  • Family resolution. get() returns null for an unknown family-and-style pair. Never assume a non-null result.

warmup() moves parsing cost from the first request to worker boot. Registry methods use pure PHP data, and lookups are constant-time map reads. Call memoryUsage() to size a worker’s resident font set against your memory budget.

A registered font can be embedded in Portable Document Format (PDF) content. Validate font provenance before registration. Do not register attacker-controlled binary data without size and format checks. Use the FontLoadedEvent hook to enforce font-licensing compliance and record which faces a document embeds.

No signing or archival normative claims apply. Font embedding and subsetting conform to the PDF 2.0 font model. The internal subsetter owns that conformance; this contract does not.

NextPDF Enterprise adds font-license attestation and an audited subsetting policy on top of the same FontRegistryInterface. Your registration code works the same across editions because the contract is the boundary.

The glossary defines font registry, image registry, and event listener; see the published glossary for the canonical definitions.