This tutorial accompanies examples/embedded-font.php. It builds a /Type0 composite font using /Identity-H, a /CIDFontType2 descendant font and every TrueType glyph in Noto Sans. The PDF displays French and Greek; the reader can select and extract the original characters thanks to the /ToUnicode stream.

This tutorial uses the optional pdftoolkit/pdf-fonts module with the core, at the published versions selected by the source manifest, to analyze TrueType. The embedded program, dictionaries and PDF mappings remain core objects. composer install in the core checkout installs the module as a development dependency; an application installs it from Packagist using composer require "pdftoolkit/pdf-fonts:^0.1.6". The package guide also explains how to select the core release series.

The demonstration font and its SIL Open Font License are preserved in the font module's resources. Composer installs them with the module; the core does not distribute the TrueType file.

Run the example

From the root of a core checkout, after composer install:

php examples/embedded-font.php embedded-font.pdf

The output path is optional; the script defaults to examples/embedded-font.pdf. Noto Sans and its license are installed in vendor/pdftoolkit/pdf-fonts/examples/assets/fonts/. The reader can search and copy French and Greek thanks to the Unicode mappings constructed by the script.

1. The font program is a separate object

The application reads the file and supplies its bytes to FontProgram. The model reads no paths and contacts no URLs. The program retains the complete bytes, its PostScript name, cmap, horizontal metrics and glyph bounding boxes.

use PDF\Fonts\TrueType\FontProgram;
use PDF\Fonts\TrueType\UnicodeText;

$program = new FontProgram($fontBytes);
$heading = new UnicodeText('Une police incorporée, objet par objet.');
$body = new UnicodeText('Français : été, cœur, Noël. Ελληνικά : Αθήνα.');

The input budget is an explicit FontProgramLimits object, defaulting to 32 MiB. The application can impose a lower limit, for example:

$program = new FontProgram($fontBytes, new FontProgramLimits(maxBytes: 1048576));

UnicodeText validates UTF-8 and retains a list of Unicode scalar values. It does not normalize text or transform its direction or typographic composition.

Font units are not directly the PDF dictionary's units. An advance of 1,024 in a font with 2,048 units per em becomes a PDF width of 500. Fractional widths are preserved. The parser also respects hmtx when the last advance record applies to several glyphs.

2. Declare the repertoire before building the graph

The CID program is first prepared and embedded in a core FontFile2. The CharacterMapFactory service receives the CID program and produces a core CharacterMap from the embedded stream and all characters that will be encoded with this instance. Each distinct Unicode character receives a distinct CID, starting at 1; CID 0 is reserved for the .notdef glyph. A repeated character reuses its CID. Two characters sharing a glyph retain two CIDs to preserve their identity during extraction.

use PDF\Document\Fonts\FontFile2;
use PDF\Fonts\TrueType\CIDFontProgram;
use PDF\Fonts\TrueType\CharacterMapFactory;
use PDF\Fonts\TrueType\TextEncoder;

$cidProgram = new CIDFontProgram($program);
$fontFile = new FontFile2($cidProgram->bytes, new FlateDecode());
$map = (new CharacterMapFactory($cidProgram))->create(
    $fontFile,
    new UnicodeText($heading->utf8 . $body->utf8),
);
$encoder = new TextEncoder($map);

A character absent from the font raises an exception when the repertoire is constructed. A character present in the font but absent from the repertoire raises an exception during encoding. There is no implicit addition, silent replacement by .notdef or state shared between documents.

3. Assemble the objects defined by PDF

CIDFontProgram prepares the program for a CID font. ISO 32000-1, 9.9 requires removing the cmap table from a TrueType program embedded through /FontFile2 for a CID font: glyph mapping comes from /CIDToGIDMap. All glyphs and other tables are retained, except a possible DSIG signature invalidated by the transformation. The sfnt directory, offsets, checksums and checksumAdjustment are recalculated.

The program is embedded by FontFile2, with its uncompressed /Length1. The /FlateDecode filter compresses the stream without changing the CID program recovered after decoding.

use PDF\Fonts\TrueType\FontDescriptorFactory;

$descriptor = (new FontDescriptorFactory($cidProgram))->create(
    $fontFile,
    new FontDescriptorFlags(4),
    new NumericType(80),
);
$glyphMap = new CIDToGIDMap($map, new FlateDecode());
$widths = new CIDWidths($map);
$descendant = new CIDFontType2($descriptor, $glyphMap, $widths);
$toUnicode = new ToUnicodeCMap($map, new FlateDecode());
$font = new Type0Font($descendant, $toUnicode);

Responsibilities are separate:

Object PDF entry or role
CIDFontProgram (module) TrueType program with every glyph, prepared for a CID font
FontFile2 Embedded TrueType CID program and /Length1
FontDescriptor Name, box, ascent, descent, angle, flags and /FontFile2
CIDToGIDMap CID → glyph-index mapping in two-byte big-endian words
CIDWidths /W, expressed in the PDF glyph system's 1,000 units
CIDFontType2 Descendant font, metrics and glyph mapping
ToUnicodeCMap Character-code → UTF-16BE mapping for extraction
Type0Font /Identity-H composite font linking the descendant and /ToUnicode

StemV is explicitly supplied: 80 PDF units in this example. TrueType defines no universal field for vertical stem thickness. This value therefore does not come from a hidden constructor estimate. CapHeight comes from OS/2 or the H glyph's box; when unavailable, the factory requires an explicit value. The core's FontDescriptor receives PDF values directly: name, box, angle and metrics. It does not read TrueType tables. Flags are explicit too: 4 is the Symbolic bit in Table 123, appropriate for this repertoire containing Greek. The Symbolic and Nonsymbolic bits cannot be combined.

Core objects check consistency of the same FontFile2 stream and repertoire. Module services connect those objects to program data. The entries they determine (/Encoding, /FontFile2, /W, /ToUnicode, etc.) cannot be replaced or removed with with() or without(). Extension entries remain explicit, immutable objects.

4. Encode the string supplied to Tj

/Identity-H takes a two-byte character code. Here that code is the CID assigned in the repertoire, not the Unicode code point or GID.

new ShowText($encoder->encode($heading));

TextEncoder returns a HexadecimalString. Objects Tf, Td, Tj, /Resources, /Page, /Pages and /Catalog are then assembled as in the first tutorials. No layout engine intervenes in this graph.

The /ToUnicode stream converts codes to UTF-16BE. Characters outside the Basic Multilingual Plane use a surrogate pair. beginbfchar blocks contain at most 100 mappings, following the CMap syntax used by the specification.

5. Explicit limits of this initial embedding support

The module embeds every glyph in the font; it does not create a subset. The sfnt binary is rebuilt to meet the CID-font constraints above. It accepts a static TrueType sfnt program with glyf outlines, Unicode cmap format 4 or 12 and horizontal metrics. CFF, CFF2, variable fonts, TTC collections, WOFF and WOFF2 are rejected. Unicode variation sequences, OpenType shaping, automatic kerning, automatic ligatures and bidirectional composition remain to be built. Characters are presented in the order supplied to the encoder.

Tables, segments, groups, metrics, loca boundaries and outline structures are bounded. Composite-glyph references are checked, along with cycles and depth. Default limits are 32 MiB per font, 256 tables, 65,535 composite-glyph references, 1,024 components per glyph, depth 64 and 1 MiB per UnicodeText object. /Identity-H permits 65,535 distinct characters in the repertoire in addition to CID 0. Table readers use bounded views of program bytes without copying every table or subtable. Format 12 cmap groups and the glyph graph remain in compact binary storage; their count does not multiply PHP-array allocations. The byte limit concerns the input program, not the process heap: the application must also budget memory for the graph and writing.

TrueType instructions are retained as data; the module does not execute them and is neither a font engine nor a sanitizer for all OpenType tables. An application accepting external fonts must choose its trust policy and, if needed, specialized sanitization before supplying bytes to the converter.

OS/2.fsType is checked: fonts with restricted embedding or bitmap-only embedding are rejected. The no-subsetting restriction is respected by full embedding. Preview & Print and Editable modes permit embedding and remain exposed through embeddingRights; the application must respect permitted uses and the font license. This field does not replace the license for redistributing font files.

References

← 08. Compressed objects · All tutorials · 10. JPEG and PNG images →

View the PDF result

This file was generated with the code in this documentation. All pages appear below; open the PDF to use its links and bookmarks.

PDF result embedded-font: page 1 of 1
Page 1 of 1
Use the browser PDF viewer

This viewer depends on your browser. If the document does not appear, use “Open the PDF” or “Download”.

Complete source code

Code from roukmoute/pdf · View the source at the documented commit ↗

Read the commented script: embedded-font.php
<?php

declare(strict_types=1);

use PDF\Document\Fonts\CIDFontType2;
use PDF\Document\Fonts\FontDescriptorFlags;
use PDF\Fonts\TrueType\CIDFontProgram;
use PDF\Fonts\TrueType\CharacterMapFactory;
use PDF\Fonts\TrueType\FontDescriptorFactory;
use PDF\Document\Fonts\CIDToGIDMap;
use PDF\Document\Fonts\CIDWidths;
use PDF\Document\Fonts\FontFile2;
use PDF\Fonts\TrueType\FontProgram;
use PDF\Fonts\TrueType\TextEncoder;
use PDF\Document\Fonts\ToUnicodeCMap;
use PDF\Fonts\TrueType\UnicodeText;
use PDF\Document\Fonts\Type0Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\FileStructure;
use PDF\Syntax\ContentStreamsResources\ContentStreams;
use PDF\Syntax\ContentStreamsResources\Operators\BeginTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\EndTextObject;
use PDF\Syntax\ContentStreamsResources\Operators\MoveTextPosition;
use PDF\Syntax\ContentStreamsResources\Operators\SetTextFontAndSize;
use PDF\Syntax\ContentStreamsResources\Operators\ShowText;
use PDF\Syntax\DataStructure\Rectangle;
use PDF\Syntax\DocumentStructure\DocumentCatalog;
use PDF\Syntax\DocumentStructure\PageTree\PageObject;
use PDF\Syntax\DocumentStructure\PageTree\PageTreeNode;
use PDF\Syntax\FileStructure\FileBody;
use PDF\Syntax\FileStructure\FileHeader;
use PDF\Syntax\Filters\FlateDecode;
use PDF\Syntax\Object\DictionaryObject\Entry;
use PDF\Syntax\Object\DictionaryType;
use PDF\Syntax\Object\IndirectReference;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;

require dirname(__DIR__) . '/vendor/autoload.php';

// Tutorial 09: docs/tutorials/09-embedded-font.md.
// The optional PDFToolkit/pdf-fonts package analyzes TrueType bytes.
// The core retains only PDF objects and their explicit values.
// File reading belongs to the application. FontProgram receives bytes
// without knowing paths, URLs or file-access mechanisms.
// Tutorial data are distributed by the module without copies in the core.
$fontBytes = file_get_contents(dirname(__DIR__) . '/vendor/pdftoolkit/pdf-fonts/examples/assets/fonts/NotoSans-Regular.ttf');
if ($fontBytes === false) {
    throw new RuntimeException('Unable to read the bundled demonstration font.');
}
$program = new FontProgram($fontBytes);
$lines = [
    new UnicodeText('Une police incorporée, objet par objet.'),
    new UnicodeText('Français : été, cœur, Noël. Ελληνικά : Αθήνα.'),
    new UnicodeText('Chaque caractère conserve son identité Unicode.'),
];
// The repertoire is known before writing: no mutable global registration.
// Two Unicode characters can have the same GID and different CIDs.
$repertoire = new UnicodeText(implode('', array_map(static fn (UnicodeText $text): string => $text->utf8, $lines)));
// The CID program retains every glyph. The specification requires removing cmap
// for FontFile2/CIDFontType2; mappings remain in our PDF objects.
// The sfnt directory and checksums are recalculated without glyph subsetting.
// Length1 measures embedded-program bytes after filter decoding.
// StemV is explicitly supplied (80 PDF units in this example), because
// TrueType has no universal field giving vertical stem thickness.
// The Symbolic flag suits this repertoire containing Greek (Table 123).
$cidProgram = new CIDFontProgram($program);
$fontFile = new FontFile2($cidProgram->bytes, new FlateDecode());
$map = (new CharacterMapFactory($cidProgram))->create($fontFile, $repertoire);
$encoder = new TextEncoder($map);
$descriptor = (new FontDescriptorFactory($cidProgram))->create($fontFile, new FontDescriptorFlags(4), new NumericType(80));
$glyphMap = new CIDToGIDMap($map, new FlateDecode());
$widths = new CIDWidths($map);
$descendant = new CIDFontType2($descriptor, $glyphMap, $widths);
$toUnicode = new ToUnicodeCMap($map, new FlateDecode());
$font = new Type0Font($descendant, $toUnicode);
$resources = new ResourceDictionary(new Font(new DictionaryType(
    new Entry(new NameType('F1'), new IndirectReference($font)),
)));

// Identity-H expects two-byte codes, not the original UTF-8 string.
// The encoder supplies these codes; ToUnicode supplies the reverse mapping
// for selection, search and extraction in PDF readers.
$contents = new StreamObject(new ContentStreams(
    new BeginTextObject(),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(22)),
    new MoveTextPosition(new NumericType(48), new NumericType(748)),
    new ShowText($encoder->encode($lines[0])),
    new SetTextFontAndSize(new NameType('F1'), new NumericType(14)),
    new MoveTextPosition(new NumericType(0), new NumericType(-42)),
    new ShowText($encoder->encode($lines[1])),
    new MoveTextPosition(new NumericType(0), new NumericType(-28)),
    new ShowText($encoder->encode($lines[2])),
    new EndTextObject(),
));
$page = new PageObject(
    new Rectangle(new NumericType(0), new NumericType(0), new NumericType(595), new NumericType(842)),
    $resources,
    $contents,
);
$pages = (new PageTreeNode())->addKid($page);
$catalog = new DocumentCatalog($pages);
$file = new FileStructure(new FileHeader('1.7'), new FileBody($catalog), new IndirectReference($catalog));
$path = $argv[1] ?? __DIR__ . '/embedded-font.pdf';
$file->writeTo($path);
echo $path . PHP_EOL;

Guide source at the documented commit ↗