Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
99.14% covered (success)
99.14%
115 / 116
80.00% covered (success)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
TrueType
99.14% covered (success)
99.14%
115 / 116
80.00% covered (success)
80.00%
4 / 5
34
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
1
 parseTtfTable
100.00% covered (success)
100.00%
30 / 30
100.00% covered (success)
100.00%
1 / 1
2
 parseName
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
5
 parseCommonTables
98.39% covered (success)
98.39%
61 / 62
0.00% covered (danger)
0.00%
0 / 1
22
 parseRequiredTables
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
4
1<?php
2declare(strict_types=1);
3/**
4 * Pop PHP Framework (https://www.popphp.org/)
5 *
6 * @link       https://github.com/popphp/popphp-framework
7 * @author     Nick Sagona, III <nick@popphp.org>
8 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
9 * @license    https://www.popphp.org/license     New BSD License
10 */
11
12/**
13 * @namespace
14 */
15namespace Pop\Pdf\Build\Font;
16
17use Pop\Utils\ArrayObject as Data;
18
19/**
20 * TrueType font class
21 *
22 * @category   Pop
23 * @package    Pop\Pdf
24 * @author     Nick Sagona, III <nick@popphp.org>
25 * @copyright  Copyright (c) 2009-2026 Nick Sagona, III
26 * @license    https://www.popphp.org/license     New BSD License
27 * @version    6.0.0
28 *
29 * @property array $rawGlyphWidths
30 * @property array $cmap
31 * @property mixed $header
32 * @property mixed $ttfHeader
33 * @property mixed $ttfTable
34 * @property array $tables
35 * @property array $tableInfo
36 */
37class TrueType extends AbstractFont
38{
39
40    /**
41     * Font properties
42     * @var array
43     */
44    protected array $properties = [
45        'info'             => null,
46        'bBox'             => null,
47        'ascent'           => 0,
48        'descent'          => 0,
49        'numberOfGlyphs'   => 0,
50        'glyphWidths'      => [],
51        'rawGlyphWidths'   => [],
52        'cmap'             => ['glyphIndexArray' => [], 'glyphNumbers' => []],
53        'missingWidth'     => 0,
54        'numberOfHMetrics' => 0,
55        'italicAngle'      => 0,
56        'capHeight'        => 0,
57        'stemH'            => 0,
58        'stemV'            => 0,
59        'unitsPerEm'       => 1000,
60        'flags'            => null,
61        'embeddable'       => true,
62        'header'           => null,
63        'ttfHeader'        => null,
64        'ttfTable'         => null,
65        'tables'           => [],
66        'tableInfo'        => []
67    ];
68
69    /**
70     * Constructor
71     *
72     * Instantiate a TrueType font file object based on a pre-existing font file on disk.
73     *
74     * @param  ?string $fontFile
75     * @param  ?string $fontStream
76     * @throws Exception|\Pop\Utils\Exception
77     */
78    public function __construct(?string $fontFile = null, ?string $fontStream = null)
79    {
80        parent::__construct($fontFile, $fontStream);
81
82        $this->parseTtfTable();
83        $this->parseName();
84        $this->parseCommonTables();
85        $this->parseRequiredTables();
86    }
87
88    /**
89     * Method to parse the TTF header and table of the TrueType font file.
90     *
91     * @return void
92     */
93    protected function parseTtfTable(): void
94    {
95        $ttfHeader = unpack(
96            'nmajorVersion/' .
97            'nminorVersion/' .
98            'nnumberOfTables/' .
99            'nsearchRange/' .
100            'nentrySelector/' .
101            'nrangeShift', $this->read(0, 12)
102        );
103
104        $tableName = $this->read(12, 4);
105
106        $ttfTable = unpack(
107            'Nchecksum/' .
108            'Noffset/' .
109            'Nlength', $this->read(16, 12)
110        );
111
112        $ttfTable['name'] = $tableName;
113
114        $this->properties['ttfHeader'] = new Data($ttfHeader);
115        $this->properties['ttfTable']  = new Data($ttfTable);
116
117        // The table directory's first entry was just parsed above (bytes
118        // 12-27) - record it in tableInfo too, then loop over the
119        // *remaining* numberOfTables - 1 entries. Looping numberOfTables
120        // times from $i = 0 here would both skip this first entry (leaving
121        // it out of tableInfo entirely) and read one entry past the real
122        // end of the directory, into the start of the first table's data.
123        $this->properties['tableInfo'][trim($tableName)] = $this->properties['ttfTable'];
124
125        $nameByteOffset  = 28;
126        $tableByteOffset = 32;
127
128        for ($i = 1; $i < $this->properties['ttfHeader']['numberOfTables']; $i++) {
129            $ttfTableName = $this->read($nameByteOffset, 4);
130            $ttfTable     = unpack(
131                'Nchecksum/' .
132                'Noffset/' .
133                'Nlength', $this->read($tableByteOffset, 12)
134            );
135
136            $this->properties['tableInfo'][trim($ttfTableName)] = new Data($ttfTable);
137
138            $nameByteOffset = $tableByteOffset + 12;
139            $tableByteOffset = $nameByteOffset + 4;
140        }
141    }
142
143    /**
144     * Method to parse the TTF info of the TrueType font file from the name table.
145     *
146     * @return void
147     */
148    protected function parseName(): void
149    {
150        if (isset($this->properties['tableInfo']['name'])) {
151            $this->properties['tables']['name'] = new TrueType\Table\Name($this);
152            $this->properties['info'] = $this->properties['tables']['name'];
153            if ((stripos($this->properties['tables']['name']['fontFamily'], 'bold') !== false) ||
154                (stripos($this->properties['tables']['name']['fullName'], 'bold') !== false) ||
155                (stripos($this->properties['tables']['name']['postscriptName'], 'bold') !== false)) {
156                $this->properties['stemV'] = 120;
157            } else {
158                $this->properties['stemV'] = 70;
159            }
160        }
161    }
162
163    /**
164     * Method to parse the common tables of the TrueType font file.
165     *
166     * @return void
167     */
168    protected function parseCommonTables(): void
169    {
170        // head
171        if (isset($this->properties['tableInfo']['head'])) {
172            $this->properties['tables']['head'] = new TrueType\Table\Head($this);
173
174            $this->properties['unitsPerEm'] = $this->properties['tables']['head']['unitsPerEm'];
175
176            $this->properties['tables']['head']['xMin'] = $this->toEmSpace($this->properties['tables']['head']['xMin']);
177            $this->properties['tables']['head']['yMin'] = $this->toEmSpace($this->properties['tables']['head']['yMin']);
178            $this->properties['tables']['head']['xMax'] = $this->toEmSpace($this->properties['tables']['head']['xMax']);
179            $this->properties['tables']['head']['yMax'] = $this->toEmSpace($this->properties['tables']['head']['yMax']);
180
181            $this->properties['bBox'] = new Data([
182                'xMin' => $this->properties['tables']['head']['xMin'],
183                'yMin' => $this->properties['tables']['head']['yMin'],
184                'xMax' => $this->properties['tables']['head']['xMax'],
185                'yMax' => $this->properties['tables']['head']['yMax']
186            ]);
187
188            $this->properties['header'] = $this->properties['tables']['head'];
189        }
190
191        // hhea
192        if (isset($this->properties['tableInfo']['hhea'])) {
193            $this->properties['tables']['hhea'] = new TrueType\Table\Hhea($this);
194            $this->properties['ascent']           = $this->properties['tables']['hhea']['ascent'];
195            $this->properties['descent']          = $this->properties['tables']['hhea']['descent'];
196            $this->properties['capHeight']        = $this->properties['ascent'] + $this->properties['descent'];
197            $this->properties['numberOfHMetrics'] = $this->properties['tables']['hhea']['numberOfHMetrics'];
198        }
199
200        // maxp
201        if (isset($this->properties['tableInfo']['maxp'])) {
202            $this->properties['tables']['maxp'] = new TrueType\Table\Maxp($this);
203            $this->properties['numberOfGlyphs'] = $this->properties['tables']['maxp']['numberOfGlyphs'];
204        }
205
206        // post
207        if (isset($this->properties['tableInfo']['post'])) {
208            $this->properties['tables']['post'] = new TrueType\Table\Post($this);
209
210            if ($this->properties['tables']['post']['italicAngle'] != 0) {
211                $this->properties['flags']['isItalic'] = true;
212                $this->properties['italicAngle'] = $this->properties['tables']['post']['italicAngle'];
213            }
214
215            if ($this->properties['tables']['post']['fixed'] != 0) {
216                $this->properties['flags']['isFixedPitch'] = true;
217            }
218        }
219
220        // hmtx
221        if (isset($this->properties['tableInfo']['hmtx'])) {
222            $this->properties['tables']['hmtx'] = new TrueType\Table\Hmtx($this);
223            $this->properties['glyphWidths'] = $this->properties['tables']['hmtx']['glyphWidths'];
224            if (isset($this->properties['glyphWidths'][0])) {
225                $this->properties['missingWidth'] = round((1000 / $this->properties['unitsPerEm']) * $this->properties['glyphWidths'][0]);
226            }
227
228            foreach ($this->properties['glyphWidths'] as $key => $value) {
229                $this->properties['rawGlyphWidths'][$key] = $value;
230                $this->properties['glyphWidths'][$key]    = round((1000 / $this->properties['unitsPerEm']) * $value);
231            }
232        }
233
234        // cmap
235        if (isset($this->properties['tableInfo']['cmap'])) {
236            $this->properties['tables']['cmap'] = new TrueType\Table\Cmap($this);
237            if (isset($this->properties['tables']['cmap']['subTables'])) {
238                $subTableIndex = null;
239                $msTable       = null;
240                $uniTable      = null;
241
242                foreach ($this->properties['tables']['cmap']['subTables'] as $index => $table) {
243                    if ($table->encoding == 'Microsoft Unicode') {
244                        $msTable = $index;
245                    } else if ($table->encoding == 'Unicode') {
246                        $uniTable = $index;
247                    }
248                }
249
250                if ($msTable !== null) {
251                    $subTableIndex = $msTable;
252                } else if ($uniTable !== null) {
253                    $subTableIndex = $uniTable;
254                } else if (isset($this->properties['tables']['cmap']['subTables'][0])) {
255                    $subTableIndex = 0;
256                }
257
258                if (($subTableIndex !== null) &&
259                    isset($this->properties['tables']['cmap']['subTables'][$subTableIndex]['parsed'])) {
260                    $parsed = $this->properties['tables']['cmap']['subTables'][$subTableIndex]['parsed'];
261                    if (isset($parsed['glyphIndexArray'])) {
262                        $this->properties['cmap']['glyphIndexArray'] = $parsed['glyphIndexArray'];
263                    }
264                    if (isset($parsed['glyphNumbers'])) {
265                        $this->properties['cmap']['glyphNumbers'] = $parsed['glyphNumbers'];
266                    }
267                }
268            }
269        }
270    }
271
272    /**
273     * Method to parse the required tables of the TrueType font file.
274     *
275     * @return void
276     */
277    protected function parseRequiredTables(): void
278    {
279        // loca
280        if (isset($this->properties['tableInfo']['loca'])) {
281            $this->properties['tables']['loca'] = new TrueType\Table\Loca($this);
282        }
283
284        // glyf
285        if (isset($this->properties['tableInfo']['glyf'])) {
286            $this->properties['tables']['glyf'] = new TrueType\Table\Glyf($this);
287        }
288
289        // OS/2 (Optional in a TTF font file)
290        if (isset($this->properties['tableInfo']['OS/2'])) {
291            $this->properties['tables']['OS/2']         = new TrueType\Table\Os2($this);
292            $this->properties['flags']['isSerif']       = $this->properties['tables']['OS/2']['flags']['isSerif'];
293            $this->properties['flags']['isScript']      = $this->properties['tables']['OS/2']['flags']['isScript'];
294            $this->properties['flags']['isSymbolic']    = $this->properties['tables']['OS/2']['flags']['isSymbolic'];
295            $this->properties['flags']['isNonSymbolic'] = $this->properties['tables']['OS/2']['flags']['isNonSymbolic'];
296            $this->properties['embeddable']             = $this->properties['tables']['OS/2']['embeddable'];
297        }
298    }
299
300}