Skip to content

Contracts / Typography

The typography domain defines the font registry and text-preprocessing contracts: FontRegistryInterface, TextPreprocessorInterface, and the immutable TextPreprocessResult and TextSegment value objects. All are stable.

Terminal window
composer require nextpdf/core:^3

FontRegistryInterface is the process-lifetime font store. It registers a TrueType, OpenType, TrueType Collection (TTC), or Printer Font Binary (PFB) font and returns parsed FontInfo metadata. Because the registry outlives individual documents, a worker parses each font only once. You can warm up a batch of fonts at boot, then lock the registry so production traffic cannot mutate it. A locked registry throws LogicException on register(), addFontDirectory(), or warmup(); lookups remain available. The registry also accepts raw font bytes through registerFromBinary(). The @font-face bridge uses this method to register a font fetched from a remote source or a data URI (uniform resource identifier). The registry stores only pure PHP data, with no resource handles, so it can be shared across a worker pool.

The engine embeds and subsets every font it uses. An embedded font program travels inside the Portable Document Format (PDF) file, so the document renders the same in any viewer, independent of installed system fonts — ISO 32000-2 §9. A font subset carries only the glyphs the document actually references. That matters most for Chinese, Japanese, and Korean (CJK) or other Unicode-rich content — ISO 32000-2 §9. The registry contract exposes the parsed metadata that the subsetting and embedding stages use.

TextPreprocessorInterface intercepts text before it enters glyph layout, font subsetting, the ToUnicode character map (CMap), and the structure tree. This placement is the security property: a preprocessor that redacts content removes it before the content can reach the content stream, the font subset, or the metadata. The contract carries two invariants. A preprocessor must not introduce layout-affecting characters, and it must preserve logical reading order; its responsibility is content substitution, not layout. The result is an immutable TextPreprocessResult with an ordered list of TextSegment values. A segment is either pass-through or redacted. For a redacted segment, the display text depends on the masking mode: empty for a black-box rectangle, asterisks matching the original length, or a fixed label. The originalCharCount on a segment is a non-reversible measurement hint used only to size a redaction rectangle. It must never be used to reconstruct the original content.

TypeKindKey membersStabilitySince
FontRegistryInterfaceinterfaceregister(), get(), has(), all(), addFontDirectory(), warmup(), lock(), isLocked(), registerBase14(), registerFromBinary(), memoryUsage()stable1.7.0
TextPreprocessorInterfaceinterfaceprocess(string): TextPreprocessResultstable1.9.0
TextPreprocessResultfinal readonly class$segments, hasRedactions(), getDisplayText()stable1.9.0
TextSegmentfinal readonly class$displayText, $isRedacted, $originalCharCount, $fillColorstable1.9.0

TextPreprocessResult and TextSegment freeze their constructor signatures and public properties; new methods may be added, but properties may not change.

examples/04-text-and-fonts.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../vendor/autoload.php';
use NextPDF\Core\Document;
$doc = Document::createStandalone();
$doc->addPage();
$doc->setFont('helvetica', 'B', 18);
$doc->cell(0, 12, 'Bold heading', newLine: true);
$doc->setFont('helvetica', '', 11);
$doc->multiCell(0, 7, 'Body text rendered with a registered font.');
$doc->save(__DIR__ . '/output/04-text-and-fonts.pdf');

setFont() resolves the family through FontRegistryInterface. A standalone document uses a private registry. In a worker, share one registry; see the document page.

examples/contracts/typography-production.php
<?php
declare(strict_types=1);
require_once __DIR__ . '/../../vendor/autoload.php';
use NextPDF\Contracts\FontRegistryInterface;
use NextPDF\Contracts\TextPreprocessorInterface;
use NextPDF\Exception\NextPdfException;
use Psr\Log\LoggerInterface;
final readonly class FontWarmupService
{
public function __construct(
private FontRegistryInterface $fonts,
private TextPreprocessorInterface $preprocessor,
private LoggerInterface $logger,
) {}
/**
* Warm a font set at boot, then lock the registry.
*
* @param list<string> $fontFiles Absolute paths to font files.
*/
public function boot(array $fontFiles): void
{
try {
$this->fonts->warmup($fontFiles);
$this->fonts->lock();
} catch (NextPdfException $e) {
$this->logger->error('Font warmup failed', ['error' => $e->getMessage()]);
throw $e;
}
}
public function redact(string $text): string
{
$result = $this->preprocessor->process($text);
return $result->hasRedactions()
? $result->getDisplayText()
: $text;
}
}

warmup() followed by lock() is the worker boot sequence. After lock(), mutation throws. Lookups continue to serve traffic.

  • A locked registry rejects every mutation method. Warm up and lock the registry at boot; never call register() during request handling.
  • registerFromBinary() writes the font bytes to a temporary file before parsing. Untrusted font data is an attack surface for the parser — gate it through ExternalResourcePolicyInterface (see the security-policy page).
  • A TextPreprocessor must not add line breaks, carriage returns, or tabs. Those characters change layout and break the contract’s first invariant.
  • TextSegment::$originalCharCount is a width hint only. Using it to infer original content defeats redaction and violates the contract’s third invariant.
  • TextPreprocessResult::getDisplayText() returns an empty string for black-box segments by design. Do not treat an empty segment as a preprocessing failure.

Font parsing dominates first use; the registry amortizes that cost to once per process. After warmup, get() and has() are O(1) map lookups. memoryUsage() returns a MemoryReport so a worker can track the font cache against its budget. Text preprocessing is linear in input length. The segment list adds bounded overhead proportional to the number of redaction matches. The performance_budget of 1500 ms wall and 64 MB peak covers warmup for a typical font set plus document rendering. Subsetting cost scales with the glyph count actually used, not the font’s full glyph table. Subsetting therefore reduces output size and rendering cost for CJK content.

The typography domain has two security-relevant surfaces. The first is font input: registerFromBinary() parses arbitrary bytes. Untrusted font data must pass an ExternalResourcePolicyInterface that bounds file size and glyph count before it reaches the parser. The second is redaction: TextPreprocessorInterface runs before glyph layout, font subsetting, the ToUnicode CMap, and the structure tree so redacted content never enters the rendered artifact. A paint-time overlay redaction leaks the original text in the content stream and the subset. The contract’s placement prevents that class of defect. The measurement hint on a segment is deliberately non-reversible. Treat any externally supplied font or text as untrusted.

ClaimStandardClauseEvidence
Every font used by the document is embedded so the document renders without relying on system fonts.ISO 32000-2§9
The embedded font is subset to the glyphs the document references.ISO 32000-2§9

Both clauses are paraphrased. NextPDF does not reproduce normative text. PDF/A-4 mandates embedding for every font. That conformance is documented on the extraction and accessibility pages.