The complete script
displays ABCDEF éΩ fi with a one-byte encoding in an embedded CMap. The font
comes from the optional conversion module; PDF objects and CMaps remain in the core.
php examples/cmap-font.php cmap-font.pdf
The Type0 font maps codes to CIDs through /Encoding. Its descendant maps CIDs
to glyphs and metrics. /ToUnicode maps original codes to Unicode text for search
and extraction. These are three distinct relationships.
The base CMap defines a codespace, a CID range and notdef mappings. The extended
CMap references it through /UseCMap, then adds codes. Codespace boundaries are
checked byte by byte; an ordinary integer interval cannot correctly represent
every multibyte range.
The Unicode CMap demonstrates bfchar, a sequential bfrange and a range using
an array of destinations. The fi code produces two Unicode characters: extracted
text can differ from glyph count. UTF-16BE and surrogate pairs are explicitly checked.
Changing codes requires consistent changes to content and ToUnicode. A CMap
does not rewrite the font program. Vertical mode and its metrics are explicit choices too.
CI verifies extracted text with pypdf and Poppler.
Reference: ISO 9.7 and 9.10, physical pages 268–292 and 299–302.
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: cmap-font.php
<?php
declare(strict_types=1);
use PDF\Document\Fonts\CIDFontType2;
use PDF\Document\Fonts\CIDToGIDMap;
use PDF\Document\Fonts\CIDWidths;
use PDF\Document\Fonts\CMaps\CMapCode;
use PDF\Document\Fonts\CMaps\CMapCodeRange;
use PDF\Document\Fonts\CMaps\CMapCodeSequence;
use PDF\Document\Fonts\CMaps\CMapName;
use PDF\Document\Fonts\CMaps\CMapSystemInfo;
use PDF\Document\Fonts\CMaps\CodeSpaceRange;
use PDF\Document\Fonts\CMaps\CodeSpaces;
use PDF\Document\Fonts\CMaps\CidChar;
use PDF\Document\Fonts\CMaps\CidMappings;
use PDF\Document\Fonts\CMaps\CidRange;
use PDF\Document\Fonts\CMaps\EncodingCMap;
use PDF\Document\Fonts\CMaps\NotdefRange;
use PDF\Document\Fonts\CMaps\UnicodeArrayRange;
use PDF\Document\Fonts\CMaps\UnicodeChar;
use PDF\Document\Fonts\CMaps\UnicodeCMap;
use PDF\Document\Fonts\CMaps\UnicodeMappings;
use PDF\Document\Fonts\CMaps\UnicodeRange;
use PDF\Document\Fonts\CMaps\UnicodeScalar;
use PDF\Document\Fonts\CMaps\UnicodeScalars;
use PDF\Document\Fonts\FontDescriptorFlags;
use PDF\Document\Fonts\FontFile2;
use PDF\Document\Fonts\StandardFont;
use PDF\Document\Fonts\ToUnicodeCMap;
use PDF\Document\Fonts\Type0Font;
use PDF\Document\Fonts\Type1Font;
use PDF\Document\ResourceDictionary;
use PDF\Document\Resources\Font;
use PDF\FileStructure;
use PDF\Fonts\TrueType\CIDFontProgram;
use PDF\Fonts\TrueType\CharacterMapFactory;
use PDF\Fonts\TrueType\FontDescriptorFactory;
use PDF\Fonts\TrueType\FontProgram;
use PDF\Fonts\TrueType\UnicodeText;
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\IntegerType;
use PDF\Syntax\Object\NameType;
use PDF\Syntax\Object\NumericType;
use PDF\Syntax\Object\StreamObject;
use PDF\Syntax\Object\StringObject\HexadecimalString;
use PDF\Syntax\Object\StringObject\LiteralString;
require dirname(__DIR__) . '/vendor/autoload.php';
// The optional module analyzes the TrueType program supplied by the application.
// The core then receives explicit CIDs, GIDs and metrics without reading a font.
$bytes = file_get_contents(dirname(__DIR__) . '/vendor/pdftoolkit/pdf-fonts/examples/assets/fonts/NotoSans-Regular.ttf');
if ($bytes === false) {
throw new RuntimeException('Cannot read the demonstration font supplied by pdf-fonts.');
}
$program = new CIDFontProgram(new FontProgram($bytes));
$fontFile = new FontFile2($program->bytes, new FlateDecode());
$map = (new CharacterMapFactory($program))->create($fontFile, new UnicodeText('ABCDEF éΩfi'));
$descriptor = (new FontDescriptorFactory($program))->create($fontFile, new FontDescriptorFlags(4), new NumericType(80));
$descendant = new CIDFontType2($descriptor, new CIDToGIDMap($map, new FlateDecode()), new CIDWidths($map));
// This CMap uses one byte per code, so it is not Identity-H. The first
// dictionary describes the codespace and six letters. The second references
// this CMap through /UseCMap and adds three distinct codes A4, A5 and A6.
$code = static fn (string $bytes): CMapCode => new CMapCode(new HexadecimalString($bytes));
$spaces = new CodeSpaces(new CodeSpaceRange($code("\0"), $code("\xff")));
$collection = new CMapSystemInfo(new LiteralString('Adobe'), new LiteralString('Identity'), new IntegerType(0));
$base = new EncodingCMap(
new CMapName(new NameType('PDFToolkit-OneByte-Base')),
$collection,
$spaces,
new CidMappings(
new CidRange(new CMapCodeRange($code('A'), $code('F')), new IntegerType($map->character(0x41)->cid)),
new CidChar($code(' '), new IntegerType($map->character(0x20)->cid)),
new NotdefRange(new CMapCodeRange($code("\0"), $code("\x1f")), new IntegerType(0)),
),
);
$encoding = new EncodingCMap(
new CMapName(new NameType('PDFToolkit-OneByte-Extended')),
$collection,
new CodeSpaces(),
new CidMappings(new CidRange(new CMapCodeRange($code("\xa4"), $code("\xa6")), new IntegerType($map->character(0xe9)->cid))),
base: $base,
);
// /Encoding selects glyphs; /ToUnicode explains their textual content.
// A..F uses incremental bfrange; A4..A6 uses distinct sequences.
// The ligature fi is one glyph whose extracted Unicode sequence is "fi".
$unicode = new ToUnicodeCMap(new UnicodeCMap(
new CMapName(new NameType('PDFToolkit-OneByte-UCS')),
$spaces,
new UnicodeMappings(
new UnicodeRange(new CMapCodeRange($code('A'), $code('F')), (new UnicodeScalar(new IntegerType(0x41)))->sequence()),
new UnicodeChar($code(' '), (new UnicodeScalar(new IntegerType(0x20)))->sequence()),
new UnicodeArrayRange(
new CMapCodeRange($code("\xa4"), $code("\xa6")),
(new UnicodeScalar(new IntegerType(0xe9)))->sequence(),
(new UnicodeScalar(new IntegerType(0x3a9)))->sequence(),
new UnicodeScalars(new UnicodeScalar(new IntegerType(0x66)), new UnicodeScalar(new IntegerType(0x69))),
),
),
), new FlateDecode());
$font = new Type0Font($descendant, $unicode, encoding: $encoding);
$resources = new ResourceDictionary(new Font(new DictionaryType(new Entry(new NameType('F1'), new IndirectReference(new Type1Font(StandardFont::Helvetica))), new Entry(new NameType('F2'), new IndirectReference($font)))));
// Each displayed code remains an object. Concatenation is checked against
// the CMap codespace; no implicit UTF-8 -> code -> CID conversion occurs here.
$characters = new CMapCodeSequence($encoding->codeSpaces(), $code('A'), $code('B'), $code('C'), $code('D'), $code('E'), $code('F'), $code(' '), $code("\xa4"), $code("\xa5"), $code(' '), $code("\xa6"));
$contents = new ContentStreams(new BeginTextObject(), new SetTextFontAndSize(new NameType('F1'), new NumericType(24)), new MoveTextPosition(new NumericType(54), new NumericType(738)), new ShowText(new LiteralString('PDFToolkit - CMaps explicites')), new EndTextObject());
foreach ([['Un encodage incorpore avec des codes sur un octet.', 684], ['Le second CMap herite du premier par UseCMap.', 658], ['CID : selection des glyphes dans la police incorporee.', 612], ['ToUnicode : extraction du sens de chaque code.', 586], ['Resultat ci-dessous : six lettres, accent, grec, ligature.', 540]] as [$text, $y]) {
$contents = $contents->with(new BeginTextObject(), new SetTextFontAndSize(new NameType('F1'), new NumericType(13)), new MoveTextPosition(new NumericType(54), new NumericType($y)), new ShowText(new LiteralString($text)), new EndTextObject());
}
$contents = $contents->with(new BeginTextObject(), new SetTextFontAndSize(new NameType('F2'), new NumericType(34)), new MoveTextPosition(new NumericType(54), new NumericType(466)), new ShowText($characters->string()), new EndTextObject());
foreach ([['A4 = e accentue ; A5 = Omega ; A6 = ligature fi.', 402], ['Les trois destinations viennent d\'un bfrange avec tableau.', 376], ['Le bfrange de A..F incremente seulement le dernier octet.', 350], ['La police et le repertoire restent des objets PDF independants.', 292], ['Source : ISO 32000-1, pages PDF 285-289 et 301-303.', 266]] as [$text, $y]) {
$contents = $contents->with(new BeginTextObject(), new SetTextFontAndSize(new NameType('F1'), new NumericType(12)), new MoveTextPosition(new NumericType(54), new NumericType($y)), new ShowText(new LiteralString($text)), new EndTextObject());
}
$page = new PageObject(new Rectangle(new NumericType(0), new NumericType(0), new NumericType(612), new NumericType(792)), $resources, new StreamObject($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__ . '/cmap-font.pdf';
$file->writeTo($path);
echo 'PDF written to ' . $path . PHP_EOL;
