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
- ISO 32000-1:2008, 9.7.4, 9.7.6, 9.8, 9.9 and 9.10.3; Tables 117, 121–123, 126 and 127.
- Microsoft OpenType: cmap.
- Microsoft OpenType: horizontal metrics.
- Microsoft OpenType: OS/2 and embedding rights.
- Microsoft OpenType: glyf.
← 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.
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;
