4 TTF.php: TrueType font file reader and writer
5 Copyright (C) 2012 Thanos Efraimidis (4real.gr)
7 This program is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or
10 (at your option) any later version.
12 This program is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
17 You should have received a copy of the GNU General Public License
18 along with this program. If not, see <http://www.gnu.org/licenses/>.
23 // Bit flags used for composite glyphs
24 const ARG_1_AND_2_ARE_WORDS
= 1;
25 const ARGS_ARE_XY_VALUES
= 2;
26 const ROUND_XY_TO_GRID
= 4;
27 const WE_HAVE_A_SCALE
= 8;
28 const MORE_COMPONENTS
= 32;
29 const WE_HAVE_AN_X_AND_Y_SCALE
= 64;
30 const WE_HAVE_A_TWO_BY_TWO
= 128;
31 const WE_HAVE_INSTRUCTIONS
= 256;
32 const USE_MY_METRICS
= 512;
35 const VERBOSE
= false;
37 private $b; // Array of bytes
38 private $tables; // Tables
40 // Constructor: parses the table directory
41 public function __construct($b)
46 $version = self
::getFixed($b, $off); // sfnt version
47 $numTables = self
::getUshort($b, $off); // number of tables
48 $searchRange = self
::getUshort($b, $off);
49 $entrySelector = self
::getUshort($b, $off);
50 $rangeShift = self
::getUshort($b, $off);
51 $this->tables
= array();
52 for ($i = 0; $i < $numTables; $i++
) {
53 $name = self
::getRaw($b, $off, 4);
54 $checksum = self
::getUlong($b, $off);
55 $offset = self
::getUlong($b, $off);
56 $length = self
::getUlong($b, $off);
57 $this->tables
[$name] = array('offset' => $offset, 'length' => $length);
60 echo sprintf("==== Table directory\n");
61 echo sprintf("Version: %s, number of tables: %d\n", $version, $numTables);
62 foreach ($this->tables
as $name => $value) {
63 echo sprintf("%s %10d %10d\n", $name, $value['offset'], $value['length']);
69 // Get raw bytes of table, or null if table does not exist
70 public function getTableRaw($name)
72 if (isset($this->tables
[$name])) {
73 $entry = $this->tables
[$name];
74 return substr($this->b
, $entry['offset'], $entry['length']);
79 ////////////////////////////////////////////////////////////////////////////////
80 // Unmarshal - marshal functions follow
81 ////////////////////////////////////////////////////////////////////////////////
82 public function unmarshalName()
85 $b = $this->getTableRaw('name');
87 $name['format'] = self
::getUshort($b, $off);
88 $name['count'] = self
::getUshort($b, $off);
89 $name['offset'] = self
::getUshort($b, $off);
90 $name['nameRecords'] = array();
92 $tmp = $name['offset'];
93 for ($i = 0; $i < $name['count']; ++
$i) {
94 $name['nameRecords'][$i] = array();
95 $name['nameRecords'][$i]['platformID'] = self
::getUshort($b, $off);
96 $name['nameRecords'][$i]['platformSpecificID'] = self
::getUshort($b, $off);
97 $name['nameRecords'][$i]['languageID'] = self
::getUshort($b, $off);
98 $name['nameRecords'][$i]['nameID'] = self
::getUshort($b, $off);
99 $name['nameRecords'][$i]['length'] = self
::getUshort($b, $off);
100 $name['nameRecords'][$i]['offset'] = self
::getUshort($b, $off);
101 $name['nameRecords'][$i]['value'] = self
::getRaw($b, $tmp, $name['nameRecords'][$i]['length'] * 2);
102 $tmp +
= $name['nameRecords'][$i]['length'] +
3;
108 public function unmarshalHead()
110 $head = array(); // To return
111 $b = $this->getTableRaw('head'); // Get raw bytes for 'head' table
113 $head['version'] = self
::getRaw($b, $off, 4); // This is actually fixed
114 $head['revision'] = self
::getRaw($b, $off, 4); // This is actually fixed
115 $off +
= 4; // Skip checksum adjustment
116 $off +
= 4; // Skip magic number
117 $head['flags'] = self
::getUshort($b, $off);
118 $head['unitsPerEm'] = self
::getUshort($b, $off);
119 $head['created'] = self
::getRaw($b, $off, 8); // This is actually longdatetime
120 $head['modified'] = self
::getRaw($b, $off, 8); // This is actually longdatetime
121 $head['xMin'] = self
::getFword($b, $off);
122 $head['yMin'] = self
::getFword($b, $off);
123 $head['xMax'] = self
::getFword($b, $off);
124 $head['yMax'] = self
::getFword($b, $off);
125 $head['macStyle'] = self
::getUshort($b, $off);
126 $head['lowestRecPPEM'] = self
::getUshort($b, $off);
127 $head['fontDirectionHint'] = self
::getShort($b, $off);
128 $head['indexToLocFormat'] = self
::getShort($b, $off);
129 $head['glyphDataFormat'] = self
::getShort($b, $off);
133 public static function marshalHead($head)
135 $b = str_repeat(chr(0), 54); // Size of 'head' is 54 bytes
137 self
::setRaw($b, $off, $head['version'], 4); // This is actually fixed
138 self
::setRaw($b, $off, $head['revision'], 4); // This is actually fixed
139 self
::setUlong($b, $off, 0); // Checksum Adjustment - will be calculated later
140 self
::setUlong($b, $off, 0x5F0F3CF5); // Magic Number
141 self
::setUshort($b, $off, $head['flags']);
142 self
::setUshort($b, $off, $head['unitsPerEm']);
143 self
::setRaw($b, $off, $head['created'], 8); // This is actually longdatetime
144 self
::setRaw($b, $off, $head['modified'], 8); // This is actually longdatetime
145 self
::setFword($b, $off, $head['xMin']);
146 self
::setFword($b, $off, $head['yMin']);
147 self
::setFword($b, $off, $head['xMax']);
148 self
::setFword($b, $off, $head['yMax']);
149 self
::setUshort($b, $off, $head['macStyle']);
150 self
::setUshort($b, $off, $head['lowestRecPPEM']);
151 self
::setShort($b, $off, $head['fontDirectionHint']);
152 self
::setShort($b, $off, $head['indexToLocFormat']);
153 self
::setShort($b, $off, $head['glyphDataFormat']);
157 public function unmarshalHhea()
159 $hhea = array(); // To return
160 $b = $this->getTableRaw('hhea'); // Get raw bytes for 'hhea' table
162 $hhea['version'] = self
::getRaw($b, $off, 4); // This is actually fixed
163 $hhea['ascender'] = self
::getFword($b, $off);
164 $hhea['descender'] = self
::getFword($b, $off);
165 $hhea['lineGap'] = self
::getFword($b, $off);
166 $hhea['advanceWidthMax'] = self
::getUFword($b, $off);
167 $hhea['minLeftSideBearing'] = self
::getFword($b, $off);
168 $hhea['minRightSideBearing'] = self
::getFword($b, $off);
169 $hhea['xMaxExtent'] = self
::getFword($b, $off);
170 $hhea['caretSlopeRise'] = self
::getShort($b, $off);
171 $hhea['caretSlopeRun'] = self
::getShort($b, $off);
172 $off +
= 10; // Skip reserved
173 $hhea['metricDataFormat'] = self
::getShort($b, $off);
174 $hhea['numberOfHMetrics'] = self
::getUShort($b, $off);
178 public static function marshalHhea($hhea)
180 $b = str_repeat(chr(0), 36); // Size of 'hhea' is 36 bytes
182 self
::setRaw($b, $off, $hhea['version'], 4); // This is actually fixed
183 self
::setFword($b, $off, $hhea['ascender']);
184 self
::setFword($b, $off, $hhea['descender']);
185 self
::setFword($b, $off, $hhea['lineGap']);
186 self
::setUFword($b, $off, $hhea['advanceWidthMax']);
187 self
::setFword($b, $off, $hhea['minLeftSideBearing']);
188 self
::setFword($b, $off, $hhea['minRightSideBearing']);
189 self
::setFword($b, $off, $hhea['xMaxExtent']);
190 self
::setShort($b, $off, $hhea['caretSlopeRise']);
191 self
::setShort($b, $off, $hhea['caretSlopeRun']);
192 $off +
= 10; // Skip reserved
193 self
::setShort($b, $off, $hhea['metricDataFormat']);
194 self
::setUshort($b, $off, $hhea['numberOfHMetrics']);
198 public function unmarshalMaxp()
200 $maxp = array(); // To return
201 $b = $this->getTableRaw('maxp'); // Get raw bytes for 'maxp' table
203 $maxp['version'] = self
::getRaw($b, $off, 4); // This is actually fixed
204 $maxp['numGlyphs'] = self
::getUshort($b, $off);
205 $maxp['maxPoints'] = self
::getUshort($b, $off);
206 $maxp['maxContours'] = self
::getUshort($b, $off);
207 $maxp['maxCompositePoints'] = self
::getUshort($b, $off);
208 $maxp['maxCompositeContours'] = self
::getUshort($b, $off);
209 $maxp['maxZones'] = self
::getUshort($b, $off);
210 $maxp['maxTwilightPoints'] = self
::getUshort($b, $off);
211 $maxp['maxStorage'] = self
::getUshort($b, $off);
212 $maxp['maxFunctionDefs'] = self
::getUshort($b, $off);
213 $maxp['maxInstructionDefs'] = self
::getUshort($b, $off);
214 $maxp['maxStackElements'] = self
::getUshort($b, $off);
215 $maxp['maxSizeOfInstructions'] = self
::getUshort($b, $off);
216 $maxp['maxComponentElements'] = self
::getUshort($b, $off);
217 $maxp['maxComponentDepth'] = self
::getUshort($b, $off);
221 public static function marshalMaxp($maxp)
223 $b = str_repeat(chr(0), 32); // Size of 'maxp' is 32 bytes
225 self
::setRaw($b, $off, $maxp['version'], 4); // This is actually fixed
226 self
::setUshort($b, $off, $maxp['numGlyphs']);
227 self
::setUshort($b, $off, $maxp['maxPoints']);
228 self
::setUshort($b, $off, $maxp['maxContours']);
229 self
::setUshort($b, $off, $maxp['maxCompositePoints']);
230 self
::setUshort($b, $off, $maxp['maxCompositeContours']);
231 self
::setUshort($b, $off, $maxp['maxZones']);
232 self
::setUshort($b, $off, $maxp['maxTwilightPoints']);
233 self
::setUshort($b, $off, $maxp['maxStorage']);
234 self
::setUshort($b, $off, $maxp['maxFunctionDefs']);
235 self
::setUshort($b, $off, $maxp['maxInstructionDefs']);
236 self
::setUshort($b, $off, $maxp['maxStackElements']);
237 self
::setUshort($b, $off, $maxp['maxSizeOfInstructions']);
238 self
::setUshort($b, $off, $maxp['maxComponentElements']);
239 self
::setUshort($b, $off, $maxp['maxComponentDepth']);
243 public function unmarshalLoca($indexToLocFormat, $numGlyphs)
245 $loca = array(); // To return
246 $b = $this->getTableRaw('loca'); // Get raw bytes for 'loca' table
248 if ($indexToLocFormat == 0) {
249 for ($i = 0; $i < $numGlyphs +
1; $i++
) {
250 $loca[] = 2 * self
::getUshort($b, $off);
253 for ($i = 0; $i < $numGlyphs +
1; $i++
) {
254 $loca[] = self
::getUlong($b, $off);
260 public static function marshalLoca($loca)
263 if ($loca[$cnt - 1] <= 0x20000) {
265 $b = str_repeat(chr(0), 2 * $cnt);
267 for ($i = 0; $i < $cnt; $i++
) {
268 self
::setUshort($b, $off, $loca[$i] / 2);
272 $b = str_repeat(chr(0), 4 * $cnt);
274 for ($i = 0; $i < $cnt; $i++
) {
275 self
::setUlong($b, $off, $loca[$i]);
281 public function unmarshalHmtx($numberOfHMetrics, $numGlyphs)
283 $metrics = array(); // To return
284 $lsbs = array(); // To return
285 $b = $this->getTableRaw('hmtx'); // Get raw bytes for 'hmtx' table
287 for ($i = 0; $i < $numberOfHMetrics; $i++
) {
288 $advanceWidth = self
::getUFword($b, $off);
289 $lsb = self
::getFword($b, $off);
290 $metrics[] = array($advanceWidth, $lsb);
292 for ($i = $numberOfHMetrics; $i < $numGlyphs; $i++
) {
293 $lsb = self
::getFword($b, $off);
296 return array('metrics' => $metrics, 'lsbs' => $lsbs);
299 public static function marshalHmtx($metrics, $lsbs)
301 $cntMetrics = count($metrics);
302 $cntLsbs = count($lsbs);
303 $b = str_repeat(chr(0), 4 * $cntMetrics +
2 * $cntLsbs);
305 for ($i = 0; $i < $cntMetrics; $i++
) {
306 $advanceWidth = $metrics[$i][0];
307 $lsb = $metrics[$i][1];
308 self
::setUFword($b, $off, $advanceWidth);
309 self
::setFword($b, $off, $lsb);
311 for ($i = 0; $i < $cntLsbs; $i++
) {
313 self
::setFword($b, $off, $lsb);
318 public function unmarshalGlyf($loca)
320 $glyf = array(); // To return
321 $b = $this->getTableRaw('glyf'); // Get raw bytes for 'glyf' table
323 $num = count($loca) - 1;
324 for ($i = 0; $i < $num; $i++
) {
325 $glyf[] = substr($b, $loca[$i], $loca[$i +
1] - $loca[$i]);
330 public static function marshalGlyf($glyf)
334 for ($i = 0; $i < $num; $i++
) {
340 public function unmarshalCmap()
342 $cmap = array(); // To return
343 $b = $this->getTableRaw('cmap'); // Get raw bytes for 'cmap' table
345 $cmap['version'] = self
::getUshort($b, $off);
346 $cmap['numTables'] = self
::getUshort($b, $off);
347 $cmap['tables'] = array();
348 $numTables = $cmap['numTables'];
349 $platformIDs = array();
350 $platformSpecificIDs = array();
352 for ($i = 0; $i < $numTables; $i++
) {
353 $platformIDs[] = self
::getUshort($b, $off);
354 $platformSpecificIDs[] = self
::getUshort($b, $off);
355 $offsets[] = self
::getUlong($b, $off);
357 for ($i = 0; $i < $numTables; $i++
) {
358 $off0 = $off = $offsets[$i];
359 $format = self
::getUshort($b, $off);
360 $length = self
::getUshort($b, $off);
361 $version = self
::getUshort($b, $off);
363 $glyphIdArray = array();
364 for ($cid = 0; $cid < 256; $cid++
) {
365 $glyphIdArray[] = self
::getByte($b, $off);
367 $cmap['tables'][] = array('platformID' => $platformIDs[$i],
368 'platformSpecificID' => $platformSpecificIDs[$i],
371 'version' => $version,
372 'glyphIdArray' => $glyphIdArray);
373 } elseif ($format == 2) {
374 throw new Exception('cmap format is 2');
375 } elseif ($format == 4) {
376 $segCountX2 = self
::getUshort($b, $off);
377 $searchRange = self
::getUshort($b, $off);
378 $entrySelector = self
::getUshort($b, $off);
379 $rangeShift = self
::getUshort($b, $off);
381 $segCount = $segCountX2 / 2;
382 $endCountArray = array();
383 $startCountArray = array();
384 $idDeltaArray = array();
385 $idRangeOffsetArray = array();
386 $glyphIdArray = array();
387 for ($seg = 0; $seg < $segCount; $seg++
) {
388 $endCountArray[] = self
::getUshort($b, $off);
390 $off +
= 2; // Skip reserved
391 for ($seg = 0; $seg < $segCount; $seg++
) {
392 $startCountArray[] = self
::getUshort($b, $off);
394 for ($seg = 0; $seg < $segCount; $seg++
) {
395 $idDeltaArray[] = self
::getUshort($b, $off);
397 for ($seg = 0; $seg < $segCount; $seg++
) {
398 $idRangeOffsetArray[] = self
::getUshort($b, $off);
400 while ($off < $off0 +
$length) {
401 $glyphIdArray[] = self
::getUshort($b, $off);
403 $cmap['tables'][] = array('platformID' => $platformIDs[$i],
404 'platformSpecificID' => $platformSpecificIDs[$i],
407 'version' => $version,
408 'segCount' => $segCount,
409 'endCountArray' => $endCountArray,
410 'startCountArray' => $startCountArray,
411 'idDeltaArray' => $idDeltaArray,
412 'idRangeOffsetArray' => $idRangeOffsetArray,
413 'glyphIdArray' => $glyphIdArray);
414 } elseif ($format == 6) {
415 $firstCode = self
::getUshort($b, $off);
416 $entryCount = self
::getUshort($b, $off);
417 $glyphIdArray = array();
418 for ($cid = $firstCode; $cid < $firstCode +
$entryCount; $cid++
) {
419 $glyphIdArray[] = self
::getUshort($b, $off);
421 $cmap['tables'][] = array('platformID' => $platformIDs[$i],
422 'platformSpecificID' => $platformSpecificIDs[$i],
425 'version' => $version,
426 'firstCode' => $firstCode,
427 'entryCount' => $entryCount,
428 'glyphIdArray' => $glyphIdArray);
430 $off -= 6; // go back and check for 8.0, 10.0 and 12.0 formats
431 $format = self
::getFixed($b, $off);
432 $length = self
::getUlong($b, $off);
433 $language = self
::getUlong($b, $off);
434 if ($format == '8.0') {
435 throw new Exception('cmap format is 8.0');
436 } elseif ($format == '10.0') {
437 throw new Exception('cmap format is 10.0');
438 } elseif ($format == '12.0') {
439 $nGroups = self
::getUlong($b, $off);
440 $startCharCodes = array();
441 $endCharCodes = array();
442 $startGlyphCodes = array();
443 for ($grp = 0; $grp < $nGroups; $grp++
) {
444 $startCharCodes[] = self
::getUlong($b, $off);
445 $endCharCodes[] = self
::getUlong($b, $off);
446 $startGlyphCodes[] = self
::getUlong($b, $off);
448 $cmap['tables'][] = array('platformID' => $platformIDs[$i],
449 'platformSpecificID' => $platformSpecificIDs[$i],
452 'version' => $version,
453 'startCharCodes' => $startCharCodes,
454 'endCharCodes' => $endCharCodes,
455 'startGlyphCodes' => $startGlyphCodes);
457 throw new Exception('Internal error: unknwon cmap format');
464 public static function marshalCmap($cmap)
466 $lengths = array(); // To hold the length of each table
468 $sz = 4 +
8 * count($cmap['tables']);
469 foreach ($cmap['tables'] as $table) {
470 $format = $table['format'];
472 $length = 6 +
256; // Size for format 0 table
473 } elseif ($format == 4) {
474 $cnt1 = count($table['startCountArray']);
475 $cnt2 = count($table['glyphIdArray']);
476 $length = 14 +
4 * 2 * $cnt1 +
2 +
2 * $cnt2; // Size for format 4 table
477 } elseif ($format == 6) {
478 $cnt = count($table['glyphIdArray']);
479 $length = 10 +
2 * $cnt; // Size for format 6 table
480 } elseif ($format == '12.0') {
481 $cnt = count($table['startCharCodes']);
482 $length = 16 +
12 * $cnt; // Size for format 12.0 table
484 throw new Exception('Internal error');
487 $lengths[] = $length;
490 $b = str_repeat(chr(0), $sz);
492 self
::setUshort($b, $off, $cmap['version']);
493 self
::setUshort($b, $off, $cmap['numTables']);
495 $offset = 4 +
8 * count($cmap['tables']);
497 foreach ($cmap['tables'] as $table) {
498 self
::setUshort($b, $off, $table['platformID']);
499 self
::setUshort($b, $off, $table['platformSpecificID']);
500 self
::setUlong($b, $off, $offset);
501 $offset +
= $lengths[$i++
];
504 $offset = 4 +
8 * count($cmap['tables']);
505 foreach ($cmap['tables'] as $table) {
508 $format = $table['format'];
509 $length = $lengths[$i];
510 $version = $table['version'];
512 self
::setUshort($b, $off, $format);
513 self
::setUshort($b, $off, $length);
514 self
::setUshort($b, $off, $version);
515 $glyphIdArray = $table['glyphIdArray'];
516 for ($cid = 0; $cid < count($glyphIdArray); $cid++
) {
517 self
::setByte($b, $off, $glyphIdArray[$cid]);
519 } elseif ($format == 4) {
520 $segCount = $table['segCount'];
521 $endCountArray = $table['endCountArray'];
522 $startCountArray = $table['startCountArray'];
523 $idDeltaArray = $table['idDeltaArray'];
524 $idRangeOffsetArray = $table['idRangeOffsetArray'];
525 $glyphIdArray = $table['glyphIdArray'];
527 // Calculate searchRange, entrySelector and rangeShift
528 $binarySearchRegisters = self
::calculateBinarySearchRegisters($segCount, 2, 1);
530 self
::setUshort($b, $off, $format);
531 self
::setUshort($b, $off, $length);
532 self
::setUshort($b, $off, $version);
533 self
::setUshort($b, $off, 2 * $segCount); // segCountX2
534 self
::setUshort($b, $off, $binarySearchRegisters['SearchRange']);
535 self
::setUshort($b, $off, $binarySearchRegisters['EntrySelector']);
536 self
::setUshort($b, $off, $binarySearchRegisters['RangeShift']);
537 for ($seg = 0; $seg < $segCount; $seg++
) {
538 self
::setUshort($b, $off, $endCountArray[$seg]);
540 self
::setUshort($b, $off, 0); // Reserved
541 for ($seg = 0; $seg < $segCount; $seg++
) {
542 self
::setUshort($b, $off, $startCountArray[$seg]);
544 for ($seg = 0; $seg < $segCount; $seg++
) {
545 self
::setUshort($b, $off, $idDeltaArray[$seg]);
547 for ($seg = 0; $seg < $segCount; $seg++
) {
548 self
::setUshort($b, $off, $idRangeOffsetArray[$seg]);
550 for ($cid = 0; $cid < count($glyphIdArray); $cid++
) {
551 self
::setUshort($b, $off, $glyphIdArray[$cid]);
553 } elseif ($format == 6) {
554 self
::setUshort($b, $off, $format);
555 self
::setUshort($b, $off, $length);
556 self
::setUshort($b, $off, $version);
557 self
::setUshort($b, $off, $table['firstCode']);
558 self
::setUshort($b, $off, $table['entryCount']);
559 $glyphIdArray = $table['glyphIdArray'];
560 for ($cid = 0; $cid < count($glyphIdArray); $cid++
) {
561 self
::setShort($b, $off, $glyphIdArray[$cid]);
563 } elseif ($format == '12.0') {
564 $startCharCodes = $table['startCharCodes'];
565 $endCharCodes = $table['endCharCodes'];
566 $startGlyphCodes = $table['startGlyphCodes'];
567 $nGroups = count($startCharCodes);
568 self
::setFixed($b, $off, '12.0');
569 self
::setUlong($b, $off, $length);
570 self
::setUlong($b, $off, 0);
571 self
::setUlong($b, $off, $nGroups);
572 for ($grp = 0; $grp < $nGroups; $grp++
) {
573 self
::setUlong($b, $off, $startCharCodes[$grp]);
574 self
::setUlong($b, $off, $endCharCodes[$grp]);
575 self
::setUlong($b, $off, $startGlyphCodes[$grp]);
578 throw new Exception('Internal error');
580 $offset +
= $lengths[$i++
];
585 public function unmarshalPost()
587 $post = array(); // To return
588 $b = $this->getTableRaw('post'); // Get raw bytes for 'post' table
590 // Collect standard header
591 $post['formatType'] = self
::getFixed($b, $off);
592 $post['italicAngle'] = self
::getFixed($b, $off);
593 $post['underlinePosition'] = self
::getFword($b, $off);
594 $post['underlineThickness'] = self
::getFword($b, $off);
595 $post['isFixedPitch'] = self
::getUlong($b, $off);
596 $post['minMemType42'] = self
::getUlong($b, $off);
597 $post['maxMemType42'] = self
::getUlong($b, $off);
598 $post['minMemType1'] = self
::getUlong($b, $off);
599 $post['maxMemType1'] = self
::getUlong($b, $off);
601 if ($post['formatType'] == '1.0') {
603 } elseif ($post['formatType'] == '2.0') {
604 // Collect numGlyphs, glyphNameIndex array and glyphNames (Pascal strings)
605 $numGlyphs = self
::getUshort($b, $off);
606 $glyphNameIndex = array();
607 for ($i = 0; $i < $numGlyphs; $i++
) {
608 $glyphNameIndex[] = self
::getUshort($b, $off);
610 $glyphNames = array();
611 while ($off < strlen($b)) {
612 $len = self
::getByte($b, $off);
613 $name = self
::getRaw($b, $off, $len);
614 $glyphNames[] = $name;
617 // 'gn' will contain either a number (for Macintosh standard order glyph name)
618 // or a string (otherwise)
620 for ($i = 0; $i < count($glyphNameIndex); $i++
) {
621 $index = $glyphNameIndex[$i];
622 if ($index >= 0 && $index <= 257) {
624 } elseif ($index >= 258 && $index <= 32767) {
625 $gn[] = $glyphNames[$index - 258];
627 throw new Exception(sprintf('Internal error - glyphNameIndex is %d', $index));
630 $post['glyphNames'] = $gn;
631 } elseif ($post['formatType'] == '3.0') {
634 throw new Exception(sprintf('Internal error - formatType is %s', $post['formatType']));
639 public static function marshalPost($post)
642 $sz = 32; // Standard header for all formatTypes
643 if ($post['formatType'] == '1.0') {
645 } elseif ($post['formatType'] == '2.0') {
646 $gn = $post['glyphNames'];
647 $sz +
= 2; // for numberOfGlyphs
648 $sz +
= 2 * count($gn); // for glyphNameIndex
649 for ($i = 0; $i < count($gn); $i++
) {
650 if (is_string($gn[$i])) {
651 $sz +
= 1 +
strlen($gn[$i]);
654 } elseif ($post['formatType'] == '3.0') {
657 throw new Exception(sprintf('Internal error - formatType is %s', $post['formatType']));
660 $b = str_repeat(chr(0), $sz);
662 self
::setFixed($b, $off, $post['formatType']);
663 self
::setFixed($b, $off, $post['italicAngle']);
664 self
::setFword($b, $off, $post['underlinePosition']);
665 self
::setFword($b, $off, $post['underlineThickness']);
666 self
::setUlong($b, $off, $post['isFixedPitch']);
667 self
::setUlong($b, $off, $post['minMemType42']);
668 self
::setUlong($b, $off, $post['maxMemType42']);
669 self
::setUlong($b, $off, $post['minMemType1']);
670 self
::setUlong($b, $off, $post['maxMemType1']);
671 if ($post['formatType'] == '1.0') {
673 } elseif ($post['formatType'] == '2.0') {
674 $gn = $post['glyphNames'];
675 $numGlyphs = count($gn);
676 $glyphNames = array();
677 self
::setUshort($b, $off, $numGlyphs); // Push numGlyphs
678 for ($i = 0; $i < $numGlyphs; $i++
) {
679 if (is_string($gn[$i])) {
680 self
::setUshort($b, $off, count($glyphNames) +
258);
681 $glyphNames[] = $gn[$i];
683 // Macintosh standard order glyph name
684 self
::setUshort($b, $off, $gn[$i]);
687 for ($i = 0; $i < count($glyphNames); $i++
) {
688 $len = strlen($glyphNames[$i]);
689 self
::setByte($b, $off, $len);
690 self
::setRaw($b, $off, $glyphNames[$i], $len);
692 } elseif ($post['formatType'] == '3.0') {
695 throw new Exception(sprintf('Internal error - formatType is %s', $post['formatType']));
700 private static $tableNamesOrderedByRank = array
701 ('head', 'hhea', 'maxp', 'OS/2', 'hmtx', 'LTSH', 'VDMX', 'hdmx', 'cmap', 'fpgm',
702 'prep', 'cvt ', 'loca', 'glyf', 'kern', 'name', 'post', 'gasp', 'PCLT', 'GDEF',
703 'GPOS', 'GSUB', 'JSTF', 'DSIG');
705 private static $tableNamesOrderedByName = array
706 ('DSIG', 'GDEF', 'GPOS', 'GSUB', 'JSTF', 'LTSH', 'OS/2', 'PCLT', 'VDMX', 'cmap',
707 'cvt ', 'fpgm', 'gasp', 'glyf', 'hdmx', 'head', 'hhea', 'hmtx', 'kern', 'loca',
708 'maxp', 'name', 'post', 'prep');
710 public static function marshalAll($tables)
712 $numTables = count($tables);
714 // Arrays to hold for each table, the checksum, the offset and the length
715 $checksums = array();
719 $sb = str_repeat(chr(0), 12 +
$numTables * 16); // Allocate room for table directory
720 foreach (self
::$tableNamesOrderedByRank as $tableName) {
721 if (isset($tables[$tableName])) {
722 $data = $tables[$tableName];
724 // Special handling for 'head' table - set checksum adjustment to zero
725 if ($tableName == 'head') {
727 self
::setUlong($data, $off, 0);
730 // Calculate checksums, offsets, lengths
731 $checksums[$tableName] = self
::calculateTableChecksum($data);
732 $offsets[$tableName] = strlen($sb);
733 $lengths[$tableName] = strlen($data);
734 // Append data and right pad with '0' (align on four byte boundary)
736 while ((strlen($sb) %
4) != 0) {
742 // Dump the table directory
744 self
::setUlong($sb, $off, 0x00010000); // This is actually fixed
745 self
::setUshort($sb, $off, $numTables);
746 // Calculate the binary search registers
747 $binarySearchRegisters = self
::calculateBinarySearchRegisters($numTables, 16, 4);
748 self
::setUshort($sb, $off, $binarySearchRegisters['SearchRange']);
749 self
::setUshort($sb, $off, $binarySearchRegisters['EntrySelector']);
750 self
::setUshort($sb, $off, $binarySearchRegisters['RangeShift']);
751 foreach (self
::$tableNamesOrderedByName as $tableName) {
752 if (isset($tables[$tableName])) {
753 self
::setRaw($sb, $off, $tableName, 4);
754 self
::setUlong($sb, $off, $checksums[$tableName]);
755 self
::setUlong($sb, $off, $offsets[$tableName]);
756 self
::setUlong($sb, $off, $lengths[$tableName]);
760 // Calculate the checksum adjustment for 'head' table
761 $checksum = self
::calculateTableChecksum(substr($sb, 0, 12 +
16 * $numTables));
762 foreach ($checksums as $chk) {
763 $checksum = bcadd($checksum, $chk);
765 $checksum = bcsub('2981146554', $checksum); // This is "0xB1B0AFBA"
766 while (bccomp($checksum, '0') < 0) {
767 $checksum = bcadd($checksum, '4294967296'); // This is "0x100000000"
769 $off = $offsets['head'] +
8;
770 self
::setUlong($sb, $off, $checksum);
774 ////////////////////////////////////////////////////////////////////////////////
775 // Helper functions follow
776 ////////////////////////////////////////////////////////////////////////////////
778 // Search "cmap" for an encoding table having given "platformID" and "platformSpecificID"
779 // and return it. Return null if no such table exists
780 public static function getEncodingTable($cmap, $platformID, $platformSpecificID)
782 foreach ($cmap['tables'] as $table) {
783 if ($table['platformID'] == 3 && $table['platformSpecificID'] == 1) {
790 // Map character "charCode" to index using the encoding table "encodingTable"
791 public static function characterToIndex($encodingTable, $charCode)
793 $format = $encodingTable['format'];
795 $glyphIdArray = $encodingTable['glyphIdArray'];
796 if ($charCode >= 0 && $charCode < 256) {
797 return $glyphIdArray[$charCode];
799 } elseif ($format == 4) {
800 $segCount = $encodingTable['segCount'];
801 $endCountArray = $encodingTable['endCountArray'];
802 $startCountArray = $encodingTable['startCountArray'];
803 $idDeltaArray = $encodingTable['idDeltaArray'];
804 $idRangeOffsetArray = $encodingTable['idRangeOffsetArray'];
805 $glyphIdArray = $encodingTable['glyphIdArray'];
807 for ($seg = 0; $seg < $segCount; $seg++
) {
808 $endCount = $endCountArray[$seg];
809 $startCount = $startCountArray[$seg];
810 $idDelta = $idDeltaArray[$seg];
811 $idRangeOffset = $idRangeOffsetArray[$seg];
812 if ($charCode >= $startCount && $charCode <= $endCount) {
813 if ($idRangeOffset != 0) {
814 $j = $charCode - $startCount +
$seg +
$idRangeOffset / 2 - $segCount;
815 $gid = $glyphIdArray[$j];
817 $gid = $idDelta +
$charCode;
819 return $gid %
= 65536;
822 } elseif ($format == 6) {
823 $firstCode = $encodingTable['firstCode'];
824 $entryCount = $encodingTable['entryCount'];
825 $glyphIdArray = $encodingTable['glyphIdArray'];
826 if ($charCode >= $firstCode && $charCode < $firstCode +
$entryCount) {
827 return $glyphIdArray[$charCode - $firstCode];
830 throw new Exception('Internal error');
835 private static function indexToCharacter($encodingTable, $gid)
837 $format = $encodingTable['format'];
839 $glyphIdArray = $encodingTable['glyphIdArray'];
840 for ($charCode = 0; $charCode < count($glyphIdArray); $charCode++
) {
841 $gid0 = $glyphIdArray[$i];
843 return sprintf("%d", $charCode);
846 } elseif ($format == 4) {
847 $segCount = $encodingTable['segCount'];
848 $endCountArray = $encodingTable['endCountArray'];
849 $startCountArray = $encodingTable['startCountArray'];
850 $idDeltaArray = $encodingTable['idDeltaArray'];
851 $idRangeOffsetArray = $encodingTable['idRangeOffsetArray'];
852 $glyphIdArray = $encodingTable['glyphIdArray'];
854 for ($seg = 0; $seg < $segCount; $seg++
) {
855 $endCount = $endCountArray[$seg];
856 $startCount = $startCountArray[$seg];
857 $idDelta = $idDeltaArray[$seg];
858 $idRangeOffset = $idRangeOffsetArray[$seg];
859 for ($charCode = $startCount; $charCode <= $endCount; $charCode++
) {
860 if ($idRangeOffset != 0) {
861 $j = $charCode - $startCount +
$seg +
$idRangeOffset / 2 - $segCount;
862 $gid0 = $glyphIdArray[$j];
864 $gid0 = $idDelta +
$charCode;
868 return sprintf("%d", $charCode);
872 } elseif ($format == 6) {
873 $firstCode = $encodingTable['firstCode'];
874 $entryCount = $encodingTable['entryCount'];
875 $glyphIdArray = $encodingTable['glyphIdArray'];
876 for ($charCode = $firstCode; $charCode < $firstCode +
$entryCount; $charCode++
) {
877 $gid0 = $glyphIdArray[$charCode - $firstCode];
879 return sprintf("%d", $charCode);
883 throw new Exception('Internal error');
888 // Get the horizontal metrics (advance width and left side bearing) for
889 // glyph with index "index"
890 public static function getHMetrics($hmtx, $numberOfHMetrics, $index)
892 $metrics = $hmtx['metrics'];
893 $lsbs = $hmtx['lsbs'];
894 if ($index < $numberOfHMetrics) {
895 return $metrics[$index];
897 // Get advance width from last element of metrics
898 return array($metrics[$numberOfHMetrics - 1][0], $lsbs[$index - $numberOfHMetrics]);
902 // Given the glyph description, parse it and return a PHP array
903 public static function getGlyph($description)
907 $numberOfContours = self
::getShort($description, $off);
908 $xMin = self
::getFword($description, $off);
909 $yMin = self
::getFword($description, $off);
910 $xMax = self
::getFword($description, $off);
911 $yMax = self
::getFword($description, $off);
912 if ($numberOfContours >= 0) {
913 // Collect the endPoints of contours. Save the last endPoint
914 $endPointsOfContours = array();
915 for ($i = 0; $i < $numberOfContours; $i++
) {
916 $lastEndPoint = self
::getUshort($description, $off);
917 $endPointsOfContours[] = $lastEndPoint;
920 // Collect the instructions
921 $instructionLength = self
::getUshort($description, $off);
922 $instructions = self
::getRaw($description, $off, $instructionLength);
926 while (count($flags) <= $lastEndPoint) {
927 $flag = ord($description{$off});
929 if (($flag & 0x08) != 0) {
930 $num = ord($description{$off}) +
1;
935 for ($j = 0; $j < $num; $j++
) {
940 // Collect the x coordinates
941 $xs = self
::getCoordinates($description, $off, $flags, 0x02, 0x10);
943 // Collect the y coordinates
944 $ys = self
::getCoordinates($description, $off, $flags, 0x04, 0x20);
946 return array('numberOfContours' => $numberOfContours,
947 'xMin' => $xMin, 'yMin' => $yMin,
948 'xMax' => $xMax, 'yMax' => $yMax,
949 'endPointsOfContours' => $endPointsOfContours,
950 'instructions' => $instructions,
951 'flags' => $flags, 'xs' => $xs, 'ys' => $ys);
953 $components = array();
956 $flags = self
::getUshort($description, $off);
957 $glyphIndex = self
::getUshort($description, $off);
959 $argument1 = $argument2 = $arg1and2 = '';
960 $scale = $xscale = $yscale = $scale01 = $scale10 = '';
962 if (($flags & self
::ARG_1_AND_2_ARE_WORDS
) != 0) {
963 $argument1 = self
::getShort($description, $off);
964 $argument2 = self
::getShort($description, $off);
966 $arg1and2 = self
::getUshort($description, $off);
968 if (($flags & self
::WE_HAVE_A_SCALE
) != 0) {
969 $scale = self
::getF2Dot14($description, $off);
970 } elseif (($flags & self
::WE_HAVE_AN_X_AND_Y_SCALE
) != 0) {
971 $xscale = self
::getF2Dot14($description, $off);
972 $yscale = self
::getF2Dot14($description, $off);
973 } elseif (($flags & self
::WE_HAVE_A_TWO_BY_TWO
) != 0) {
974 $xscale = self
::getF2Dot14($description, $off);
975 $scale01 = self
::getF2Dot14($description, $off);
976 $scale10 = self
::getF2Dot14($description, $off);
977 $yscale = self
::getF2Dot14($description, $off);
981 echo sprintf("arg1=[%s], arg2=[%s], arg1and2=[%s]\n", $argument1, $argument2, $arg1and2);
982 echo sprintf("flags=0x%02x, glyphIndex=%d\n", $flags, $glyphIndex);
983 if (($flags & self
::ARG_1_AND_2_ARE_WORDS
) != 0) {
984 echo " arg1and2areWords";
986 if (($flags & self
::ARGS_ARE_XY_VALUES
) != 0) {
987 echo " argsAreXyValues";
989 if (($flags & self
::ROUND_XY_TO_GRID
) != 0) {
990 echo " roundXyToGrid";
992 if (($flags & self
::WE_HAVE_A_SCALE
) != 0) {
993 echo " weHaveAScale";
995 if (($flags & self
::MORE_COMPONENTS
) != 0) {
996 echo " moreComponents";
998 if (($flags & self
::WE_HAVE_AN_X_AND_Y_SCALE
) != 0) {
999 echo " weHaveAnXandYscale";
1001 if (($flags & self
::WE_HAVE_A_TWO_BY_TWO
) != 0) {
1002 echo " weHaveATwoByTwo";
1004 if (($flags & self
::WE_HAVE_INSTRUCTIONS
) != 0) {
1005 echo " weHaveInstructions";
1007 if (($flags & self
::USE_MY_METRICS
) != 0) {
1008 echo " useMyMetrics";
1013 $components[] = array('flags' => $flags, 'glyphIndex' => $glyphIndex,
1014 'argument1' => $argument1, 'argument2' => $argument2, 'arg1and2' => $arg1and2,
1015 'scale' => $scale, 'xscale' => $xscale, 'yscale' => $yscale, 'scale01' => $scale01, 'scale10' => $scale10);
1016 } while (($flags & self
::MORE_COMPONENTS
) != 0);
1017 if (($flags & self
::WE_HAVE_INSTRUCTIONS
) != 0) {
1018 $numInstr = self
::getUshort($description, $off);
1019 $instructions = self
::getRaw($description, $off, $numInstr);
1023 return array('numberOfContours' => $numberOfContours,
1024 'xMin' => $xMin, 'yMin' => $yMin,
1025 'xMax' => $xMax, 'yMax' => $yMax,
1026 'components' => $components,
1027 'instructions' => $instructions);
1031 // Replace glyph indices of components of composite glyph
1032 public static function replaceComponentsOfCompositeGlyph($description, $replacements)
1036 $numberOfContours = self
::getShort($description, $off);
1037 if ($numberOfContours >= 0) {
1038 return $description;
1040 $off +
= 8; // Skip xMin, yMin, xMax, yMax
1042 $flags = self
::getUshort($description, $off);
1043 $glyphIndex = self
::getUshort($description, $off);
1044 if (isset($replacements[$glyphIndex])) {
1045 $from = $glyphIndex;
1046 $to = $replacements[$from];
1047 $off -= 2; // Go back and replace
1048 self
::setUshort($description, $off, $to);
1051 if (($flags & self
::ARG_1_AND_2_ARE_WORDS
) != 0) {
1056 if (($flags & self
::WE_HAVE_A_SCALE
) != 0) {
1058 } elseif (($flags & self
::WE_HAVE_AN_X_AND_Y_SCALE
) != 0) {
1060 } elseif (($flags & self
::WE_HAVE_A_TWO_BY_TWO
) != 0) {
1063 } while (($flags & self
::MORE_COMPONENTS
) != 0);
1064 return $description;
1068 // Calculate searchRange, entrySelector and rangeShift
1069 private static function calculateBinarySearchRegisters($count, $size, $logSize)
1071 $entrySelector = -$logSize;
1073 while (2 * $searchRange < $count * $size) {
1077 $rangeShift = $count * $size - $searchRange;
1078 return array('SearchRange' => $searchRange, 'EntrySelector' => $entrySelector, 'RangeShift' => $rangeShift);
1081 private static function calculateTableChecksum($data)
1085 // "Right" pad with zeros
1086 while ((strlen($data) %
4) != 0) {
1090 $len = strlen($data);
1091 while ($off < $len) {
1092 $ret +
= self
::getUlong($data, $off);
1094 $ret = bcmod($ret, '4294967296');
1098 //////////////////// Function to get and set bytes, shorts, longs, etc ////////////////////
1099 private static function getByte($b, &$off)
1101 return ord($b[$off++
]);
1104 private static function setByte(&$b, &$off, $val)
1106 $b{$off++
} = chr($val);
1109 private static function getUshort($b, &$off)
1111 $num = ord($b[$off++
]);
1112 $num = 256 * $num +
ord($b[$off++
]);
1116 private static function setUshort(&$b, &$off, $val)
1118 $b{$off++
} = chr($val / 256);
1119 $b{$off++
} = chr($val %
256);
1122 private static function getShort($b, &$off)
1124 $num = self
::getUshort($b, $off);
1125 return $num < 32768 ?
$num : $num - 65536;
1128 private static function setShort(&$b, &$off, $val)
1130 $b{$off++
} = chr(($val >> 8) & 0xff);
1131 $b{$off++
} = chr($val & 0xff);
1134 private static function getUlong($b, &$off)
1137 $ret = bcadd($ret, bcmul(ord($b[$off++
]), '16777216'));
1138 $ret = bcadd($ret, bcmul(ord($b[$off++
]), '65536'));
1139 $ret = bcadd($ret, bcmul(ord($b[$off++
]), '256'));
1140 $ret = bcadd($ret, ord($b[$off++
]));
1144 private static function setUlong(&$b, &$off, $val)
1146 $b{$off++
} = chr(bcmod(bcdiv($val, '16777216', 0), '256'));
1147 $b{$off++
} = chr(bcmod(bcdiv($val, '65536', 0), '256'));
1148 $b{$off++
} = chr(bcmod(bcdiv($val, '256', 0), '256'));
1149 $b{$off++
} = chr(bcmod($val, '256'));
1152 private static function getLong($b, &$off)
1154 $ret = self
::getUlong($b, $off);
1155 return bccomp($ret, '2147483648') < 0 ?
$ret : bcsub($ret, '4294967296');
1158 private static function getFixed($b, &$off)
1160 $b1 = ord($b[$off++
]);
1161 $b2 = ord($b[$off++
]);
1162 $b3 = ord($b[$off++
]);
1163 $b4 = ord($b[$off++
]);
1165 $mantissa = $b1 * 256 +
$b2;
1166 if ($mantissa >= 32768) {
1169 $fraction = $b3 * 256 +
$b4;
1171 if ($fraction == 0) {
1172 return sprintf("%d.0", $mantissa); // Append one zero
1174 $tmp = sprintf("%.6f", $fraction / 65536);
1175 $tmp = substr($tmp, 2); // Remove leading "0."
1176 return sprintf("%d.%s", $mantissa, $tmp);
1180 private static function setFixed(&$b, &$off, $val)
1182 if ($val{0} == '-') {
1184 $val = substr($val, 1);
1188 if (($idx = strpos($val, '.')) === false) {
1189 $mantissa = intval($val);
1192 $mantissa = intval(substr($val, 0, $idx));
1193 $fraction = intval(substr($val, $idx +
1));
1197 $b{$off++
} = chr(($mantissa >> 8) & 0xff);
1198 $b{$off++
} = chr(($mantissa >> 0) & 0xff);
1199 $b{$off++
} = chr(($fraction >> 8) & 0xff);
1200 $b{$off++
} = chr(($fraction >> 0) & 0xff);
1203 private static function getFword($b, &$off)
1205 return self
::getShort($b, $off);
1208 private static function setUFword(&$b, &$off, $val)
1210 self
::setUshort($b, $off, $val);
1213 private static function getUFword($b, &$off)
1215 return self
::getUshort($b, $off);
1218 private static function setFword(&$b, &$off, $val)
1220 self
::setShort($b, $off, $val);
1223 private static function getF2dot14($b, &$off)
1225 $val1 = ord($b{$off});
1226 $val2 = ord($b{$off +
1});
1227 $val = 256 * $val1 +
$val2;
1229 $mantissa = ($val >> 14) & 0x03;
1230 if ($mantissa >= 2) {
1233 $fraction = $val & 0x3fff;
1235 if ($fraction == 0) {
1236 // Append only one zero
1237 $ret = sprintf("%d.0", $mantissa);
1239 $tmp = sprintf("%.6f", $fraction / 16384);
1240 $tmp = substr($tmp, 2); // Remove leading "0."
1241 $ret = sprintf("%d.%s", $mantissa, $tmp);
1246 private static function getRaw($b, &$off, $num)
1248 $ret = substr($b, $off, $num);
1253 private static function setRaw(&$b, &$off, $val, $num)
1257 $b{$off++
} = $val{$i++
};
1261 private static function parseFixed($val)
1268 $mantissa = $b1 * 256 +
$b2;
1269 if ($mantissa >= 32768) {
1272 $fraction = $b3 * 256 +
$b4;
1274 if ($fraction == 0) {
1275 // Append only one zero
1276 return sprintf("%d.0", $mantissa);
1278 $tmp = sprintf("%.6f", $fraction / 65536);
1279 $tmp = substr($tmp, 2); // Remove leading ".0"
1280 return sprintf("%d.%s", $mantissa, $tmp);
1284 private static function getCoordinates($code, &$off, $flags, $mask1, $mask2)
1287 for ($i = 0; $i < count($flags); $i++
) {
1289 $bit1 = $flag & $mask1;
1290 $bit4 = $flag & $mask2;
1292 $b = ord($code{$off++
});
1302 // Same as previous (delta=0)
1306 $b1 = ord($code{$off++
});
1307 $b2 = ord($code{$off++
});
1308 $b = 256 * $b1 +
$b2;