ttfparser.php (17951B)
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 ?>