tc-lib-pdf-font

Technical guide for integrating tc-lib-pdf-font, generating fonts, and adding custom font families

Overview

tc-lib-pdf-font provides font import and runtime font-stack services for PDF composition engines.

It sits between the font files on disk and the text a document draws, holding the metrics, the encodings, and the tooling that turns a TTF or an OTF into something a PDF can embed.

In practice, there are two phases:

  1. Build phase: convert source fonts (.ttf, .pfb, .afm) into font definition assets.
  2. Runtime phase: load those generated assets through the font stack when rendering PDF content.

Repository and API Docs

Project Metadata

ItemValue
Namespace\Com\Tecnick\Pdf\Font
LicenseGNU LGPL v3

Installation

composer require tecnickcom/tc-lib-pdf-font

For full PDF rendering with this font engine, install tecnickcom/tc-lib-pdf in your application.

composer require tecnickcom/tc-lib-pdf

Where It Fits

Whenever a document needs a font that is not one of the 14 standard ones: a brand typeface, coverage for a script the core fonts do not have, or a subset small enough to ship.

Features

Font Processing

  • Import support for core, Type1, and TrueType sources
  • Font metadata extraction and normalization
  • Utilities for subset and output dictionary generation
  • Optional injectable cache to reuse computed TrueType font subsets
  • Font definition files are read through Definition::normalize(), which reads each member at the type Load::DEFAULT_DATA declares and drops unreadable or undeclared members
  • Every PDF name written by Output is escaped, and a descriptor entry whose escaped name is empty is dropped

Runtime Font Stack

  • Font stack insertion and switching
  • Glyph width and bounding-box helpers
  • Character replacement and fallback handling
  • Glyph index encoding of composite fonts, with support for the supplementary planes
  • Subsetting mode is aggregated across repeated requests for the same font, so a font is subset only when every request asked for one
  • Non-finite font size, spacing and stretching values are rejected

How Integration Works

At runtime, the library resolves and loads generated JSON font-definition files, then emits the required PDF font objects and embedded font streams.

The normal application flow is:

  1. Define the font directory with generated assets.
  2. Insert a font into the stack (family + style + size).
  3. Use the returned font output token in page content.
  4. Let PDF output generation embed font program data as needed.

Font Discovery Rules

When you call insert(), font definition lookup follows this order:

  1. Explicit definition file path (when provided).
  2. K_PATH_FONTS.
  3. Subdirectories under K_PATH_FONTS.
  4. Library fallback font directories.

If a style-specific file is missing, the loader can fall back to the base family and apply artificial bold/italic adjustments.

Font paths are normalized (trailing separators are stripped) and every font file is resolved and validated through the resolveLocalPath() and isAllowedFile() helpers of tc-lib-file before it is read.

Character Encoding of Composite Fonts

A TrueTypeUnicode font is emitted as a composite (Type0) font with the Identity-H encoding, so every character code in a content stream is 2 bytes wide.

Since version 4.0 the character code is the glyph index of the font (CID == GID): the font dictionary declares /CIDToGIDMap /Identity, the /W array is keyed by glyph index, and a /ToUnicode CMap is generated for the glyphs used by the document so that the text stays searchable and extractable. Earlier versions used the Unicode codepoint as the character code and embedded a 131072-byte CIDToGIDMap stream, which limited the addressable characters to the Basic Multilingual Plane.

Encoding a String

Composite text must be encoded through the font stack, so that the glyphs used are recorded for the /W array and the /ToUnicode CMap:

$stack = new \Com\Tecnick\Pdf\Font\Stack(1.0);
$stack->insert($objnum, 'dejavusans', '', 12);

if ($stack->isCurrentGidEncoded()) {
    // 2-byte big-endian glyph indices, and the glyphs are recorded on the font
    $codes = $stack->ordArrToGidStr([0x41, 0x20, 0x1D703]);
} else {
    // CID-0 fonts keep the UTF-16BE encoding expected by their predefined CMap
    $codes = $uniconv->toUTF16BE($str);
}

The text object written to the page must also select the font the string was encoded with, otherwise the glyph indices are resolved against a different font. This does not apply to fonts that are not GID encoded, whose character codes mean the same thing in every font.

Relevant methods:

MethodPurpose
Stack::isCurrentGidEncoded()True when the current font encodes text as glyph indices.
Stack::ordArrToGidStr(array $uniarr)Encodes codepoints as 2-byte glyph indices and records them.
Stack::getGidForOrd(int $ord)Glyph index of a codepoint, 0 when the font has no glyph for it.
Buffer::addUsedGid(string $key, int $gid, int $ord)Records a glyph and the codepoint it was encoded from.

Font Definition Files

The glyph index of a codepoint is read at runtime from the .ctg.z artifact of the font, so definition files generated by earlier versions keep working for the whole Basic Multilingual Plane without being regenerated. That file must remain next to the .json definition file.

Codepoints above U+FFFF do not fit that table and are stored in the definition file under the ctgu key:

"ctgu": {"120579": 1588, "119886": 941}

That key is only written when the font is converted with --encoding_id=10, which selects the format 12 cmap subtable. A definition file carrying ctgu is still readable by older versions of the library, which ignore it.

Optional Font Subset Cache

Generating a font subset is expensive. The library can reuse subsets across Output instances and PHP processes through an optional external cache that you provide. No backend is shipped and caching is disabled by default, so behavior is unchanged unless you opt in.

Implement \Com\Tecnick\Pdf\Font\FontSubsetCacheInterface and inject it as the last Output constructor argument:

interface FontSubsetCacheInterface
{
    public function get(string $key): ?string;          // null on cache miss
    public function set(string $key, string $subsetFont): void;
}
$output = new \Com\Tecnick\Pdf\Font\Output(
    $fonts,
    $objectNumber,
    $encrypt,
    $fileHelper,   // or null
    $subsetCache,  // your FontSubsetCacheInterface implementation
);

The cache key already folds in the font program bytes, cmap-selection metrics, and the requested subset characters, so distinct inputs never collide. The library never evicts entries: the injected backend owns expiration and size limits. The interface is intentionally trivial to wrap around any backend (PSR-16/PSR-6, Symfony Cache, Redis, APCu, and so on).

Integration Notes

Record where each licensed font came from and what its licence allows. Embedding a font in a PDF is a redistribution, and some licences say so explicitly.

Subsetting is on by default and is usually what you want, since it embeds only the glyphs the document uses. Turn it off for a form whose fields will be filled in later with characters the document never contained.

Test the multilingual paths with real data. A missing glyph does not stop generation: it falls back or drops out, and the only place that shows is the rendered page, or a getWarnings() entry in a conformance mode.

Generate Fonts with Makefile

Run these commands from the tc-lib-pdf-font repository root. The Makefile and the util/ directory are part of the distributed package, so the same commands work from vendor/tecnickcom/tc-lib-pdf-font in a consuming project.

Most Common Commands

# Install dependencies and clean target fonts
make deps

# Import and convert bundled font sets into target/fonts
make fonts

Full Build Pipeline

make buildall

buildall includes dependency setup, code checks, font generation, QA, and packaging steps.

What make fonts Runs

make fonts executes the utility build flow:

  1. Installs the production dependencies of the package, so the target also works on a standalone checkout inside vendor/.
  2. Installs utility dependencies in util/.
  3. Runs util/bulk_convert.php.
  4. Writes generated assets under target/fonts/<family>/.

util/convert.php resolves its Composer autoloader and output path relative to the package, so it can be invoked from any working directory.

Packaging Targets for Font Data

make rpm_fonts
make deb_fonts
make bz2_fonts

Utility-Level Build (Direct)

cd util
make deps
make build

Add Custom Fonts

Use the converter utility to import your own font files into a writable font directory.

1) Convert a Font File

php util/convert.php \
	--outpath=/absolute/path/to/fonts \
	--fonts=/absolute/path/MyFont-Regular.ttf

2) Convert Style Variants

Convert bold, italic, and bold-italic files separately so style keys resolve naturally:

php util/convert.php --outpath=/absolute/path/to/fonts --fonts=/fonts/MyFont-Bold.ttf
php util/convert.php --outpath=/absolute/path/to/fonts --fonts=/fonts/MyFont-Italic.ttf
php util/convert.php --outpath=/absolute/path/to/fonts --fonts=/fonts/MyFont-BoldItalic.ttf

3) Point Runtime to Output Directory

define('K_PATH_FONTS', '/absolute/path/to/fonts');

Then load with:

$regular = $pdf->font->insert($pdf->pon, 'myfont', '', 10);
$bold = $pdf->font->insert($pdf->pon, 'myfont', 'B', 10);

4) Optional Explicit Import Settings

# TrueType Unicode
php util/convert.php --outpath=/fonts --type=TrueTypeUnicode --fonts=/fonts/MyFont-Regular.ttf

# Type1 with cp1252 encoding
php util/convert.php --outpath=/fonts --type=Type1 --encoding=cp1252 --fonts=/fonts/MyType1.pfb

Converter Options

--fonts accepts a comma-separated list, so several files can be converted in one call:

php util/convert.php \
	--outpath=./target/fonts/custom/ \
	--type=TrueTypeUnicode \
	--flags=32 \
	--encoding_id=10 \
	--fonts=/path/to/MyFont-Regular.ttf,/path/to/MyFont-Bold.ttf
OptionDescription
--outpathDirectory the generated font definition files are written to.
--fontsComma-separated list of input font files.
--typeExplicit font type (TrueTypeUnicode, TrueType, Type1, CID0JP, CID0KR, CID0CS, CID0CT). Leave empty for autodetect.
--encodingEncoding table (for example cp1252 for many non-Unicode Type1/Core cases). Omit for Unicode and symbolic fonts.
--flagsPDF descriptor flags. Default is 32 (non-symbolic).
--platform_id, --encoding_idCMAP selection for TrueType Unicode imports (defaults 3 and 1). --encoding_id=10 reads the format 12 subtable, required for characters above U+FFFF; a font without that subtable falls back to the BMP one.
--linkedLink to a system font file instead of embedding or copying it (not transportable).

Run php util/convert.php --help for the full usage text.

util/bulk_convert.php drives batch generation from the mirrored font set and also attempts OTF conversion through FontForge (fontforge -script otf2ttf.ff ...) before import. Its destination is set with --outpath.

Naming and Compatibility Notes

  • Generated output names are normalized to lowercase.
  • Name normalization maps common suffixes: bold -> b, italic/oblique -> i, regular -> ''.
  • Re-importing the same normalized font name in the same output directory will fail.
  • Cloning a font family with a different style loads the definition file matching that style, so style variants no longer inherit the metrics of the base family.
  • OpenType CFF (OTTO) is not supported directly.
  • In PDF/A mode, CID0 fonts are not supported.

Typed Enums

\Com\Tecnick\Pdf\Font\FontType is a backed enum of the supported font types: Auto ('', selects the type automatically), Core, TrueType, TrueTypeUnicode, Type1, and the CID-0 variants CID0JP, CID0KR, CID0CS, CID0CT. It can be used wherever the type string was previously accepted, including the --type value of the conversion utility.

Requirements

  • PHP 8.2 or later
  • Extensions: hash, json, pcre, zlib
  • Optional extensions:
    • mbstring: decodes UTF-16BE and Windows-1252 TrueType name strings
    • iconv: decodes legacy Macintosh (MacRoman) TrueType name strings
  • Package dependencies: tecnickcom/tc-lib-file, tecnickcom/tc-lib-pdf-encrypt, tecnickcom/tc-lib-unicode-data
  • Composer

Upgrading from 3.x to 4.0

Version 4.0 changes the character codes emitted for TrueTypeUnicode fonts from Unicode codepoints to glyph indices, as described in Character Encoding of Composite Fonts. A consumer of the library must be updated together with it:

  • Encode composite text with Stack::ordArrToGidStr() instead of writing UTF-16BE codepoints, and make the text object select the font the string was encoded with. A consumer that keeps writing codepoints produces PDFs whose text renders as unrelated glyphs, with no error raised.
  • A TrueTypeUnicode font emits one PDF object less, since the CIDToGIDMap stream is no longer embedded. Baselines that compare PDF output byte by byte, or that assert on object numbers, must be regenerated.
  • The TFontData shape gained the ctgu, gidenc, and usedgid keys. Code that builds that array must add them.

Unchanged:

  • No method was removed or changed signature; the new members are additions.
  • Font definition files and .ctg.z artifacts generated by 3.x keep working for the whole Basic Multilingual Plane. Regenerate a font only to gain the characters above U+FFFF, with --encoding_id=10.
  • The output of Core, TrueType, Type1, and cidfont0 fonts is unchanged.

Unicode Data Dependency

The bidirectional class of a code point is resolved through Type::getBidiClass() rather than by indexing Type::UNI directly, so the package needs a tc-lib-unicode-data release that exposes that accessor. Composer resolves the constraint; upgrade tc-lib-unicode in the same step, since the two share those tables.

Example

<?php

require_once __DIR__ . '/vendor/autoload.php';

$font = new \Com\Tecnick\Pdf\Font\Import('/path/to/font.ttf');
$metrics = $font->getFontMetrics();

var_dump($font->getFontName(), $metrics['type']);

Support and Contribution