Copybara bot | be50d49 | 2023-11-30 00:16:42 +0100 | [diff] [blame^] | 1 | <?php
|
| 2 | /*******************************************************************************
|
| 3 | * Class to parse and subset TrueType fonts *
|
| 4 | * *
|
| 5 | * Version: 1.11 *
|
| 6 | * Date: 2021-04-18 *
|
| 7 | * Author: Olivier PLATHEY *
|
| 8 | *******************************************************************************/
|
| 9 |
|
| 10 | class TTFParser
|
| 11 | {
|
| 12 | protected $f;
|
| 13 | protected $tables;
|
| 14 | protected $numberOfHMetrics;
|
| 15 | protected $numGlyphs;
|
| 16 | protected $glyphNames;
|
| 17 | protected $indexToLocFormat;
|
| 18 | protected $subsettedChars;
|
| 19 | protected $subsettedGlyphs;
|
| 20 | public $chars;
|
| 21 | public $glyphs;
|
| 22 | public $unitsPerEm;
|
| 23 | public $xMin, $yMin, $xMax, $yMax;
|
| 24 | public $postScriptName;
|
| 25 | public $embeddable;
|
| 26 | public $bold;
|
| 27 | public $typoAscender;
|
| 28 | public $typoDescender;
|
| 29 | public $capHeight;
|
| 30 | public $italicAngle;
|
| 31 | public $underlinePosition;
|
| 32 | public $underlineThickness;
|
| 33 | public $isFixedPitch;
|
| 34 |
|
| 35 | function __construct($file)
|
| 36 | {
|
| 37 | $this->f = fopen($file, 'rb');
|
| 38 | if(!$this->f)
|
| 39 | $this->Error('Can\'t open file: '.$file);
|
| 40 | }
|
| 41 |
|
| 42 | function __destruct()
|
| 43 | {
|
| 44 | if(is_resource($this->f))
|
| 45 | fclose($this->f);
|
| 46 | }
|
| 47 |
|
| 48 | function Parse()
|
| 49 | {
|
| 50 | $this->ParseOffsetTable();
|
| 51 | $this->ParseHead();
|
| 52 | $this->ParseHhea();
|
| 53 | $this->ParseMaxp();
|
| 54 | $this->ParseHmtx();
|
| 55 | $this->ParseLoca();
|
| 56 | $this->ParseGlyf();
|
| 57 | $this->ParseCmap();
|
| 58 | $this->ParseName();
|
| 59 | $this->ParseOS2();
|
| 60 | $this->ParsePost();
|
| 61 | }
|
| 62 |
|
| 63 | function ParseOffsetTable()
|
| 64 | {
|
| 65 | $version = $this->Read(4);
|
| 66 | if($version=='OTTO')
|
| 67 | $this->Error('OpenType fonts based on PostScript outlines are not supported');
|
| 68 | if($version!="\x00\x01\x00\x00")
|
| 69 | $this->Error('Unrecognized file format');
|
| 70 | $numTables = $this->ReadUShort();
|
| 71 | $this->Skip(3*2); // searchRange, entrySelector, rangeShift
|
| 72 | $this->tables = array();
|
| 73 | for($i=0;$i<$numTables;$i++)
|
| 74 | {
|
| 75 | $tag = $this->Read(4);
|
| 76 | $checkSum = $this->Read(4);
|
| 77 | $offset = $this->ReadULong();
|
| 78 | $length = $this->ReadULong();
|
| 79 | $this->tables[$tag] = array('offset'=>$offset, 'length'=>$length, 'checkSum'=>$checkSum);
|
| 80 | }
|
| 81 | }
|
| 82 |
|
| 83 | function ParseHead()
|
| 84 | {
|
| 85 | $this->Seek('head');
|
| 86 | $this->Skip(3*4); // version, fontRevision, checkSumAdjustment
|
| 87 | $magicNumber = $this->ReadULong();
|
| 88 | if($magicNumber!=0x5F0F3CF5)
|
| 89 | $this->Error('Incorrect magic number');
|
| 90 | $this->Skip(2); // flags
|
| 91 | $this->unitsPerEm = $this->ReadUShort();
|
| 92 | $this->Skip(2*8); // created, modified
|
| 93 | $this->xMin = $this->ReadShort();
|
| 94 | $this->yMin = $this->ReadShort();
|
| 95 | $this->xMax = $this->ReadShort();
|
| 96 | $this->yMax = $this->ReadShort();
|
| 97 | $this->Skip(3*2); // macStyle, lowestRecPPEM, fontDirectionHint
|
| 98 | $this->indexToLocFormat = $this->ReadShort();
|
| 99 | }
|
| 100 |
|
| 101 | function ParseHhea()
|
| 102 | {
|
| 103 | $this->Seek('hhea');
|
| 104 | $this->Skip(4+15*2);
|
| 105 | $this->numberOfHMetrics = $this->ReadUShort();
|
| 106 | }
|
| 107 |
|
| 108 | function ParseMaxp()
|
| 109 | {
|
| 110 | $this->Seek('maxp');
|
| 111 | $this->Skip(4);
|
| 112 | $this->numGlyphs = $this->ReadUShort();
|
| 113 | }
|
| 114 |
|
| 115 | function ParseHmtx()
|
| 116 | {
|
| 117 | $this->Seek('hmtx');
|
| 118 | $this->glyphs = array();
|
| 119 | for($i=0;$i<$this->numberOfHMetrics;$i++)
|
| 120 | {
|
| 121 | $advanceWidth = $this->ReadUShort();
|
| 122 | $lsb = $this->ReadShort();
|
| 123 | $this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
|
| 124 | }
|
| 125 | for($i=$this->numberOfHMetrics;$i<$this->numGlyphs;$i++)
|
| 126 | {
|
| 127 | $lsb = $this->ReadShort();
|
| 128 | $this->glyphs[$i] = array('w'=>$advanceWidth, 'lsb'=>$lsb);
|
| 129 | }
|
| 130 | }
|
| 131 |
|
| 132 | function ParseLoca()
|
| 133 | {
|
| 134 | $this->Seek('loca');
|
| 135 | $offsets = array();
|
| 136 | if($this->indexToLocFormat==0)
|
| 137 | {
|
| 138 | // Short format
|
| 139 | for($i=0;$i<=$this->numGlyphs;$i++)
|
| 140 | $offsets[] = 2*$this->ReadUShort();
|
| 141 | }
|
| 142 | else
|
| 143 | {
|
| 144 | // Long format
|
| 145 | for($i=0;$i<=$this->numGlyphs;$i++)
|
| 146 | $offsets[] = $this->ReadULong();
|
| 147 | }
|
| 148 | for($i=0;$i<$this->numGlyphs;$i++)
|
| 149 | {
|
| 150 | $this->glyphs[$i]['offset'] = $offsets[$i];
|
| 151 | $this->glyphs[$i]['length'] = $offsets[$i+1] - $offsets[$i];
|
| 152 | }
|
| 153 | }
|
| 154 |
|
| 155 | function ParseGlyf()
|
| 156 | {
|
| 157 | $tableOffset = $this->tables['glyf']['offset'];
|
| 158 | foreach($this->glyphs as &$glyph)
|
| 159 | {
|
| 160 | if($glyph['length']>0)
|
| 161 | {
|
| 162 | fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
|
| 163 | if($this->ReadShort()<0)
|
| 164 | {
|
| 165 | // Composite glyph
|
| 166 | $this->Skip(4*2); // xMin, yMin, xMax, yMax
|
| 167 | $offset = 5*2;
|
| 168 | $a = array();
|
| 169 | do
|
| 170 | {
|
| 171 | $flags = $this->ReadUShort();
|
| 172 | $index = $this->ReadUShort();
|
| 173 | $a[$offset+2] = $index;
|
| 174 | if($flags & 1) // ARG_1_AND_2_ARE_WORDS
|
| 175 | $skip = 2*2;
|
| 176 | else
|
| 177 | $skip = 2;
|
| 178 | if($flags & 8) // WE_HAVE_A_SCALE
|
| 179 | $skip += 2;
|
| 180 | elseif($flags & 64) // WE_HAVE_AN_X_AND_Y_SCALE
|
| 181 | $skip += 2*2;
|
| 182 | elseif($flags & 128) // WE_HAVE_A_TWO_BY_TWO
|
| 183 | $skip += 4*2;
|
| 184 | $this->Skip($skip);
|
| 185 | $offset += 2*2 + $skip;
|
| 186 | }
|
| 187 | while($flags & 32); // MORE_COMPONENTS
|
| 188 | $glyph['components'] = $a;
|
| 189 | }
|
| 190 | }
|
| 191 | }
|
| 192 | }
|
| 193 |
|
| 194 | function ParseCmap()
|
| 195 | {
|
| 196 | $this->Seek('cmap');
|
| 197 | $this->Skip(2); // version
|
| 198 | $numTables = $this->ReadUShort();
|
| 199 | $offset31 = 0;
|
| 200 | for($i=0;$i<$numTables;$i++)
|
| 201 | {
|
| 202 | $platformID = $this->ReadUShort();
|
| 203 | $encodingID = $this->ReadUShort();
|
| 204 | $offset = $this->ReadULong();
|
| 205 | if($platformID==3 && $encodingID==1)
|
| 206 | $offset31 = $offset;
|
| 207 | }
|
| 208 | if($offset31==0)
|
| 209 | $this->Error('No Unicode encoding found');
|
| 210 |
|
| 211 | $startCount = array();
|
| 212 | $endCount = array();
|
| 213 | $idDelta = array();
|
| 214 | $idRangeOffset = array();
|
| 215 | $this->chars = array();
|
| 216 | fseek($this->f, $this->tables['cmap']['offset']+$offset31, SEEK_SET);
|
| 217 | $format = $this->ReadUShort();
|
| 218 | if($format!=4)
|
| 219 | $this->Error('Unexpected subtable format: '.$format);
|
| 220 | $this->Skip(2*2); // length, language
|
| 221 | $segCount = $this->ReadUShort()/2;
|
| 222 | $this->Skip(3*2); // searchRange, entrySelector, rangeShift
|
| 223 | for($i=0;$i<$segCount;$i++)
|
| 224 | $endCount[$i] = $this->ReadUShort();
|
| 225 | $this->Skip(2); // reservedPad
|
| 226 | for($i=0;$i<$segCount;$i++)
|
| 227 | $startCount[$i] = $this->ReadUShort();
|
| 228 | for($i=0;$i<$segCount;$i++)
|
| 229 | $idDelta[$i] = $this->ReadShort();
|
| 230 | $offset = ftell($this->f);
|
| 231 | for($i=0;$i<$segCount;$i++)
|
| 232 | $idRangeOffset[$i] = $this->ReadUShort();
|
| 233 |
|
| 234 | for($i=0;$i<$segCount;$i++)
|
| 235 | {
|
| 236 | $c1 = $startCount[$i];
|
| 237 | $c2 = $endCount[$i];
|
| 238 | $d = $idDelta[$i];
|
| 239 | $ro = $idRangeOffset[$i];
|
| 240 | if($ro>0)
|
| 241 | fseek($this->f, $offset+2*$i+$ro, SEEK_SET);
|
| 242 | for($c=$c1;$c<=$c2;$c++)
|
| 243 | {
|
| 244 | if($c==0xFFFF)
|
| 245 | break;
|
| 246 | if($ro>0)
|
| 247 | {
|
| 248 | $gid = $this->ReadUShort();
|
| 249 | if($gid>0)
|
| 250 | $gid += $d;
|
| 251 | }
|
| 252 | else
|
| 253 | $gid = $c+$d;
|
| 254 | if($gid>=65536)
|
| 255 | $gid -= 65536;
|
| 256 | if($gid>0)
|
| 257 | $this->chars[$c] = $gid;
|
| 258 | }
|
| 259 | }
|
| 260 | }
|
| 261 |
|
| 262 | function ParseName()
|
| 263 | {
|
| 264 | $this->Seek('name');
|
| 265 | $tableOffset = $this->tables['name']['offset'];
|
| 266 | $this->postScriptName = '';
|
| 267 | $this->Skip(2); // format
|
| 268 | $count = $this->ReadUShort();
|
| 269 | $stringOffset = $this->ReadUShort();
|
| 270 | for($i=0;$i<$count;$i++)
|
| 271 | {
|
| 272 | $this->Skip(3*2); // platformID, encodingID, languageID
|
| 273 | $nameID = $this->ReadUShort();
|
| 274 | $length = $this->ReadUShort();
|
| 275 | $offset = $this->ReadUShort();
|
| 276 | if($nameID==6)
|
| 277 | {
|
| 278 | // PostScript name
|
| 279 | fseek($this->f, $tableOffset+$stringOffset+$offset, SEEK_SET);
|
| 280 | $s = $this->Read($length);
|
| 281 | $s = str_replace(chr(0), '', $s);
|
| 282 | $s = preg_replace('|[ \[\](){}<>/%]|', '', $s);
|
| 283 | $this->postScriptName = $s;
|
| 284 | break;
|
| 285 | }
|
| 286 | }
|
| 287 | if($this->postScriptName=='')
|
| 288 | $this->Error('PostScript name not found');
|
| 289 | }
|
| 290 |
|
| 291 | function ParseOS2()
|
| 292 | {
|
| 293 | $this->Seek('OS/2');
|
| 294 | $version = $this->ReadUShort();
|
| 295 | $this->Skip(3*2); // xAvgCharWidth, usWeightClass, usWidthClass
|
| 296 | $fsType = $this->ReadUShort();
|
| 297 | $this->embeddable = ($fsType!=2) && ($fsType & 0x200)==0;
|
| 298 | $this->Skip(11*2+10+4*4+4);
|
| 299 | $fsSelection = $this->ReadUShort();
|
| 300 | $this->bold = ($fsSelection & 32)!=0;
|
| 301 | $this->Skip(2*2); // usFirstCharIndex, usLastCharIndex
|
| 302 | $this->typoAscender = $this->ReadShort();
|
| 303 | $this->typoDescender = $this->ReadShort();
|
| 304 | if($version>=2)
|
| 305 | {
|
| 306 | $this->Skip(3*2+2*4+2);
|
| 307 | $this->capHeight = $this->ReadShort();
|
| 308 | }
|
| 309 | else
|
| 310 | $this->capHeight = 0;
|
| 311 | }
|
| 312 |
|
| 313 | function ParsePost()
|
| 314 | {
|
| 315 | $this->Seek('post');
|
| 316 | $version = $this->ReadULong();
|
| 317 | $this->italicAngle = $this->ReadShort();
|
| 318 | $this->Skip(2); // Skip decimal part
|
| 319 | $this->underlinePosition = $this->ReadShort();
|
| 320 | $this->underlineThickness = $this->ReadShort();
|
| 321 | $this->isFixedPitch = ($this->ReadULong()!=0);
|
| 322 | if($version==0x20000)
|
| 323 | {
|
| 324 | // Extract glyph names
|
| 325 | $this->Skip(4*4); // min/max usage
|
| 326 | $this->Skip(2); // numberOfGlyphs
|
| 327 | $glyphNameIndex = array();
|
| 328 | $names = array();
|
| 329 | $numNames = 0;
|
| 330 | for($i=0;$i<$this->numGlyphs;$i++)
|
| 331 | {
|
| 332 | $index = $this->ReadUShort();
|
| 333 | $glyphNameIndex[] = $index;
|
| 334 | if($index>=258 && $index-257>$numNames)
|
| 335 | $numNames = $index-257;
|
| 336 | }
|
| 337 | for($i=0;$i<$numNames;$i++)
|
| 338 | {
|
| 339 | $len = ord($this->Read(1));
|
| 340 | $names[] = $this->Read($len);
|
| 341 | }
|
| 342 | foreach($glyphNameIndex as $i=>$index)
|
| 343 | {
|
| 344 | if($index>=258)
|
| 345 | $this->glyphs[$i]['name'] = $names[$index-258];
|
| 346 | else
|
| 347 | $this->glyphs[$i]['name'] = $index;
|
| 348 | }
|
| 349 | $this->glyphNames = true;
|
| 350 | }
|
| 351 | else
|
| 352 | $this->glyphNames = false;
|
| 353 | }
|
| 354 |
|
| 355 | function Subset($chars)
|
| 356 | {
|
| 357 | $this->subsettedGlyphs = array();
|
| 358 | $this->AddGlyph(0);
|
| 359 | $this->subsettedChars = array();
|
| 360 | foreach($chars as $char)
|
| 361 | {
|
| 362 | if(isset($this->chars[$char]))
|
| 363 | {
|
| 364 | $this->subsettedChars[] = $char;
|
| 365 | $this->AddGlyph($this->chars[$char]);
|
| 366 | }
|
| 367 | }
|
| 368 | }
|
| 369 |
|
| 370 | function AddGlyph($id)
|
| 371 | {
|
| 372 | if(!isset($this->glyphs[$id]['ssid']))
|
| 373 | {
|
| 374 | $this->glyphs[$id]['ssid'] = count($this->subsettedGlyphs);
|
| 375 | $this->subsettedGlyphs[] = $id;
|
| 376 | if(isset($this->glyphs[$id]['components']))
|
| 377 | {
|
| 378 | foreach($this->glyphs[$id]['components'] as $cid)
|
| 379 | $this->AddGlyph($cid);
|
| 380 | }
|
| 381 | }
|
| 382 | }
|
| 383 |
|
| 384 | function Build()
|
| 385 | {
|
| 386 | $this->BuildCmap();
|
| 387 | $this->BuildHhea();
|
| 388 | $this->BuildHmtx();
|
| 389 | $this->BuildLoca();
|
| 390 | $this->BuildGlyf();
|
| 391 | $this->BuildMaxp();
|
| 392 | $this->BuildPost();
|
| 393 | return $this->BuildFont();
|
| 394 | }
|
| 395 |
|
| 396 | function BuildCmap()
|
| 397 | {
|
| 398 | if(!isset($this->subsettedChars))
|
| 399 | return;
|
| 400 |
|
| 401 | // Divide charset in contiguous segments
|
| 402 | $chars = $this->subsettedChars;
|
| 403 | sort($chars);
|
| 404 | $segments = array();
|
| 405 | $segment = array($chars[0], $chars[0]);
|
| 406 | for($i=1;$i<count($chars);$i++)
|
| 407 | {
|
| 408 | if($chars[$i]>$segment[1]+1)
|
| 409 | {
|
| 410 | $segments[] = $segment;
|
| 411 | $segment = array($chars[$i], $chars[$i]);
|
| 412 | }
|
| 413 | else
|
| 414 | $segment[1]++;
|
| 415 | }
|
| 416 | $segments[] = $segment;
|
| 417 | $segments[] = array(0xFFFF, 0xFFFF);
|
| 418 | $segCount = count($segments);
|
| 419 |
|
| 420 | // Build a Format 4 subtable
|
| 421 | $startCount = array();
|
| 422 | $endCount = array();
|
| 423 | $idDelta = array();
|
| 424 | $idRangeOffset = array();
|
| 425 | $glyphIdArray = '';
|
| 426 | for($i=0;$i<$segCount;$i++)
|
| 427 | {
|
| 428 | list($start, $end) = $segments[$i];
|
| 429 | $startCount[] = $start;
|
| 430 | $endCount[] = $end;
|
| 431 | if($start!=$end)
|
| 432 | {
|
| 433 | // Segment with multiple chars
|
| 434 | $idDelta[] = 0;
|
| 435 | $idRangeOffset[] = strlen($glyphIdArray) + ($segCount-$i)*2;
|
| 436 | for($c=$start;$c<=$end;$c++)
|
| 437 | {
|
| 438 | $ssid = $this->glyphs[$this->chars[$c]]['ssid'];
|
| 439 | $glyphIdArray .= pack('n', $ssid);
|
| 440 | }
|
| 441 | }
|
| 442 | else
|
| 443 | {
|
| 444 | // Segment with a single char
|
| 445 | if($start<0xFFFF)
|
| 446 | $ssid = $this->glyphs[$this->chars[$start]]['ssid'];
|
| 447 | else
|
| 448 | $ssid = 0;
|
| 449 | $idDelta[] = $ssid - $start;
|
| 450 | $idRangeOffset[] = 0;
|
| 451 | }
|
| 452 | }
|
| 453 | $entrySelector = 0;
|
| 454 | $n = $segCount;
|
| 455 | while($n!=1)
|
| 456 | {
|
| 457 | $n = $n>>1;
|
| 458 | $entrySelector++;
|
| 459 | }
|
| 460 | $searchRange = (1<<$entrySelector)*2;
|
| 461 | $rangeShift = 2*$segCount - $searchRange;
|
| 462 | $cmap = pack('nnnn', 2*$segCount, $searchRange, $entrySelector, $rangeShift);
|
| 463 | foreach($endCount as $val)
|
| 464 | $cmap .= pack('n', $val);
|
| 465 | $cmap .= pack('n', 0); // reservedPad
|
| 466 | foreach($startCount as $val)
|
| 467 | $cmap .= pack('n', $val);
|
| 468 | foreach($idDelta as $val)
|
| 469 | $cmap .= pack('n', $val);
|
| 470 | foreach($idRangeOffset as $val)
|
| 471 | $cmap .= pack('n', $val);
|
| 472 | $cmap .= $glyphIdArray;
|
| 473 |
|
| 474 | $data = pack('nn', 0, 1); // version, numTables
|
| 475 | $data .= pack('nnN', 3, 1, 12); // platformID, encodingID, offset
|
| 476 | $data .= pack('nnn', 4, 6+strlen($cmap), 0); // format, length, language
|
| 477 | $data .= $cmap;
|
| 478 | $this->SetTable('cmap', $data);
|
| 479 | }
|
| 480 |
|
| 481 | function BuildHhea()
|
| 482 | {
|
| 483 | $this->LoadTable('hhea');
|
| 484 | $numberOfHMetrics = count($this->subsettedGlyphs);
|
| 485 | $data = substr_replace($this->tables['hhea']['data'], pack('n',$numberOfHMetrics), 4+15*2, 2);
|
| 486 | $this->SetTable('hhea', $data);
|
| 487 | }
|
| 488 |
|
| 489 | function BuildHmtx()
|
| 490 | {
|
| 491 | $data = '';
|
| 492 | foreach($this->subsettedGlyphs as $id)
|
| 493 | {
|
| 494 | $glyph = $this->glyphs[$id];
|
| 495 | $data .= pack('nn', $glyph['w'], $glyph['lsb']);
|
| 496 | }
|
| 497 | $this->SetTable('hmtx', $data);
|
| 498 | }
|
| 499 |
|
| 500 | function BuildLoca()
|
| 501 | {
|
| 502 | $data = '';
|
| 503 | $offset = 0;
|
| 504 | foreach($this->subsettedGlyphs as $id)
|
| 505 | {
|
| 506 | if($this->indexToLocFormat==0)
|
| 507 | $data .= pack('n', $offset/2);
|
| 508 | else
|
| 509 | $data .= pack('N', $offset);
|
| 510 | $offset += $this->glyphs[$id]['length'];
|
| 511 | }
|
| 512 | if($this->indexToLocFormat==0)
|
| 513 | $data .= pack('n', $offset/2);
|
| 514 | else
|
| 515 | $data .= pack('N', $offset);
|
| 516 | $this->SetTable('loca', $data);
|
| 517 | }
|
| 518 |
|
| 519 | function BuildGlyf()
|
| 520 | {
|
| 521 | $tableOffset = $this->tables['glyf']['offset'];
|
| 522 | $data = '';
|
| 523 | foreach($this->subsettedGlyphs as $id)
|
| 524 | {
|
| 525 | $glyph = $this->glyphs[$id];
|
| 526 | fseek($this->f, $tableOffset+$glyph['offset'], SEEK_SET);
|
| 527 | $glyph_data = $this->Read($glyph['length']);
|
| 528 | if(isset($glyph['components']))
|
| 529 | {
|
| 530 | // Composite glyph
|
| 531 | foreach($glyph['components'] as $offset=>$cid)
|
| 532 | {
|
| 533 | $ssid = $this->glyphs[$cid]['ssid'];
|
| 534 | $glyph_data = substr_replace($glyph_data, pack('n',$ssid), $offset, 2);
|
| 535 | }
|
| 536 | }
|
| 537 | $data .= $glyph_data;
|
| 538 | }
|
| 539 | $this->SetTable('glyf', $data);
|
| 540 | }
|
| 541 |
|
| 542 | function BuildMaxp()
|
| 543 | {
|
| 544 | $this->LoadTable('maxp');
|
| 545 | $numGlyphs = count($this->subsettedGlyphs);
|
| 546 | $data = substr_replace($this->tables['maxp']['data'], pack('n',$numGlyphs), 4, 2);
|
| 547 | $this->SetTable('maxp', $data);
|
| 548 | }
|
| 549 |
|
| 550 | function BuildPost()
|
| 551 | {
|
| 552 | $this->Seek('post');
|
| 553 | if($this->glyphNames)
|
| 554 | {
|
| 555 | // Version 2.0
|
| 556 | $numberOfGlyphs = count($this->subsettedGlyphs);
|
| 557 | $numNames = 0;
|
| 558 | $names = '';
|
| 559 | $data = $this->Read(2*4+2*2+5*4);
|
| 560 | $data .= pack('n', $numberOfGlyphs);
|
| 561 | foreach($this->subsettedGlyphs as $id)
|
| 562 | {
|
| 563 | $name = $this->glyphs[$id]['name'];
|
| 564 | if(is_string($name))
|
| 565 | {
|
| 566 | $data .= pack('n', 258+$numNames);
|
| 567 | $names .= chr(strlen($name)).$name;
|
| 568 | $numNames++;
|
| 569 | }
|
| 570 | else
|
| 571 | $data .= pack('n', $name);
|
| 572 | }
|
| 573 | $data .= $names;
|
| 574 | }
|
| 575 | else
|
| 576 | {
|
| 577 | // Version 3.0
|
| 578 | $this->Skip(4);
|
| 579 | $data = "\x00\x03\x00\x00";
|
| 580 | $data .= $this->Read(4+2*2+5*4);
|
| 581 | }
|
| 582 | $this->SetTable('post', $data);
|
| 583 | }
|
| 584 |
|
| 585 | function BuildFont()
|
| 586 | {
|
| 587 | $tags = array();
|
| 588 | foreach(array('cmap', 'cvt ', 'fpgm', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'prep') as $tag)
|
| 589 | {
|
| 590 | if(isset($this->tables[$tag]))
|
| 591 | $tags[] = $tag;
|
| 592 | }
|
| 593 | $numTables = count($tags);
|
| 594 | $offset = 12 + 16*$numTables;
|
| 595 | foreach($tags as $tag)
|
| 596 | {
|
| 597 | if(!isset($this->tables[$tag]['data']))
|
| 598 | $this->LoadTable($tag);
|
| 599 | $this->tables[$tag]['offset'] = $offset;
|
| 600 | $offset += strlen($this->tables[$tag]['data']);
|
| 601 | }
|
| 602 |
|
| 603 | // Build offset table
|
| 604 | $entrySelector = 0;
|
| 605 | $n = $numTables;
|
| 606 | while($n!=1)
|
| 607 | {
|
| 608 | $n = $n>>1;
|
| 609 | $entrySelector++;
|
| 610 | }
|
| 611 | $searchRange = 16*(1<<$entrySelector);
|
| 612 | $rangeShift = 16*$numTables - $searchRange;
|
| 613 | $offsetTable = pack('nnnnnn', 1, 0, $numTables, $searchRange, $entrySelector, $rangeShift);
|
| 614 | foreach($tags as $tag)
|
| 615 | {
|
| 616 | $table = $this->tables[$tag];
|
| 617 | $offsetTable .= $tag.$table['checkSum'].pack('NN', $table['offset'], $table['length']);
|
| 618 | }
|
| 619 |
|
| 620 | // Compute checkSumAdjustment (0xB1B0AFBA - font checkSum)
|
| 621 | $s = $this->CheckSum($offsetTable);
|
| 622 | foreach($tags as $tag)
|
| 623 | $s .= $this->tables[$tag]['checkSum'];
|
| 624 | $a = unpack('n2', $this->CheckSum($s));
|
| 625 | $high = 0xB1B0 + ($a[1]^0xFFFF);
|
| 626 | $low = 0xAFBA + ($a[2]^0xFFFF) + 1;
|
| 627 | $checkSumAdjustment = pack('nn', $high+($low>>16), $low);
|
| 628 | $this->tables['head']['data'] = substr_replace($this->tables['head']['data'], $checkSumAdjustment, 8, 4);
|
| 629 |
|
| 630 | $font = $offsetTable;
|
| 631 | foreach($tags as $tag)
|
| 632 | $font .= $this->tables[$tag]['data'];
|
| 633 |
|
| 634 | return $font;
|
| 635 | }
|
| 636 |
|
| 637 | function LoadTable($tag)
|
| 638 | {
|
| 639 | $this->Seek($tag);
|
| 640 | $length = $this->tables[$tag]['length'];
|
| 641 | $n = $length % 4;
|
| 642 | if($n>0)
|
| 643 | $length += 4 - $n;
|
| 644 | $this->tables[$tag]['data'] = $this->Read($length);
|
| 645 | }
|
| 646 |
|
| 647 | function SetTable($tag, $data)
|
| 648 | {
|
| 649 | $length = strlen($data);
|
| 650 | $n = $length % 4;
|
| 651 | if($n>0)
|
| 652 | $data = str_pad($data, $length+4-$n, "\x00");
|
| 653 | $this->tables[$tag]['data'] = $data;
|
| 654 | $this->tables[$tag]['length'] = $length;
|
| 655 | $this->tables[$tag]['checkSum'] = $this->CheckSum($data);
|
| 656 | }
|
| 657 |
|
| 658 | function Seek($tag)
|
| 659 | {
|
| 660 | if(!isset($this->tables[$tag]))
|
| 661 | $this->Error('Table not found: '.$tag);
|
| 662 | fseek($this->f, $this->tables[$tag]['offset'], SEEK_SET);
|
| 663 | }
|
| 664 |
|
| 665 | function Skip($n)
|
| 666 | {
|
| 667 | fseek($this->f, $n, SEEK_CUR);
|
| 668 | }
|
| 669 |
|
| 670 | function Read($n)
|
| 671 | {
|
| 672 | return $n>0 ? fread($this->f, $n) : '';
|
| 673 | }
|
| 674 |
|
| 675 | function ReadUShort()
|
| 676 | {
|
| 677 | $a = unpack('nn', fread($this->f,2));
|
| 678 | return $a['n'];
|
| 679 | }
|
| 680 |
|
| 681 | function ReadShort()
|
| 682 | {
|
| 683 | $a = unpack('nn', fread($this->f,2));
|
| 684 | $v = $a['n'];
|
| 685 | if($v>=0x8000)
|
| 686 | $v -= 65536;
|
| 687 | return $v;
|
| 688 | }
|
| 689 |
|
| 690 | function ReadULong()
|
| 691 | {
|
| 692 | $a = unpack('NN', fread($this->f,4));
|
| 693 | return $a['N'];
|
| 694 | }
|
| 695 |
|
| 696 | function CheckSum($s)
|
| 697 | {
|
| 698 | $n = strlen($s);
|
| 699 | $high = 0;
|
| 700 | $low = 0;
|
| 701 | for($i=0;$i<$n;$i+=4)
|
| 702 | {
|
| 703 | $high += (ord($s[$i])<<8) + ord($s[$i+1]);
|
| 704 | $low += (ord($s[$i+2])<<8) + ord($s[$i+3]);
|
| 705 | }
|
| 706 | return pack('nn', $high+($low>>16), $low);
|
| 707 | }
|
| 708 |
|
| 709 | function Error($msg)
|
| 710 | {
|
| 711 | throw new Exception($msg);
|
| 712 | }
|
| 713 | }
|
| 714 | ?>
|