1<?php 2 3namespace Mpdf; 4 5use Mpdf\Fonts\FontCache; 6use Mpdf\Fonts\GlyphOperator; 7 8// NOTE*** If you change the defined constants below, be sure to delete all temporary font data files in /ttfontdata/ 9// to force mPDF to regenerate cached font files. 10if (!defined('_OTL_OLD_SPEC_COMPAT_2')) { 11 define('_OTL_OLD_SPEC_COMPAT_2', true); 12} 13 14// Define the value used in the "head" table of a created TTF file 15// 0x74727565 "true" for Mac 16// 0x00010000 for Windows 17// Either seems to work for a font embedded in a PDF file 18// when read by Adobe Reader on a Windows PC(!) 19if (!defined('_TTF_MAC_HEADER')) { 20 define('_TTF_MAC_HEADER', false); 21} 22 23// Recalculate correct metadata/profiles when making subset fonts (not SIP/SMP) 24// e.g. xMin, xMax, maxNContours 25if (!defined('_RECALC_PROFILE')) { 26 define('_RECALC_PROFILE', false); 27} 28 29// mPDF 5.7.1 30if (!function_exists('\Mpdf\unicode_hex')) { 31 function unicode_hex($unicode_dec) 32 { 33 return sprintf("%05s", strtoupper(dechex($unicode_dec))); 34 } 35} 36 37/** 38 * TTFontFile class 39 * 40 * This class is based on The ReportLab Open Source PDF library 41 * written in Python - http://www.reportlab.com/software/opensource/ 42 * together with ideas from the OpenOffice source code and others. 43 * This header must be retained in any redistribution or 44 * modification of the file. 45 * 46 * @author Ian Back <ianb@bpm1.com> 47 * @license LGPL 48 */ 49class TTFontFile 50{ 51 52 use Strict; 53 54 private $fontCache; 55 56 private $fontDescriptor; 57 58 var $GPOSFeatures; 59 60 var $GPOSLookups; 61 62 var $GPOSScriptLang; 63 64 var $MarkAttachmentType; 65 66 var $MarkGlyphSets; 67 68 var $GlyphClassMarks; 69 70 var $GlyphClassLigatures; 71 72 var $GlyphClassBases; 73 74 var $GlyphClassComponents; 75 76 var $GSUBScriptLang; 77 78 var $rtlPUAstr; 79 80 var $fontkey; 81 82 var $useOTL; 83 84 var $maxUni; 85 86 var $sFamilyClass; 87 88 var $sFamilySubClass; 89 90 var $sipset; 91 92 var $smpset; 93 94 var $_pos; 95 96 var $numTables; 97 98 var $searchRange; 99 100 var $entrySelector; 101 102 var $rangeShift; 103 104 var $tables; 105 106 var $otables; 107 108 var $filename; 109 110 var $fh; 111 112 var $glyphPos; 113 114 var $charToGlyph; 115 116 var $ascent; 117 118 var $descent; 119 120 var $lineGap; 121 122 var $hheaascent; 123 124 var $hheadescent; 125 126 var $hhealineGap; 127 128 var $advanceWidthMax; 129 130 var $typoAscender; 131 132 var $typoDescender; 133 134 var $typoLineGap; 135 136 var $usWinAscent; 137 138 var $usWinDescent; 139 140 var $strikeoutSize; 141 142 var $strikeoutPosition; 143 144 var $name; 145 146 var $familyName; 147 148 var $styleName; 149 150 var $fullName; 151 152 var $uniqueFontID; 153 154 var $unitsPerEm; 155 156 var $bbox; 157 158 var $capHeight; 159 160 var $xHeight; 161 162 var $stemV; 163 164 var $italicAngle; 165 166 var $flags; 167 168 var $underlinePosition; 169 170 var $underlineThickness; 171 172 var $charWidths; 173 174 var $defaultWidth; 175 176 var $maxStrLenRead; 177 178 var $numTTCFonts; 179 180 var $TTCFonts; 181 182 var $maxUniChar; 183 184 var $kerninfo; 185 186 var $haskernGPOS; 187 188 var $hassmallcapsGSUB; 189 190 var $codeToGlyph; 191 192 var $glyphdata; 193 194 var $LuCoverage; 195 196 public $panose; 197 198 public $version; 199 200 public $fontRevision; 201 202 public $restrictedUse; 203 204 public $glyphIDtoUni; 205 206 public $glyphToChar; 207 208 public $GSUBFeatures; 209 210 public $GSUBLookups; 211 212 public $GSLuCoverage; 213 214 public function __construct(FontCache $fontCache, $fontDescriptor) 215 { 216 $this->fontCache = $fontCache; 217 $this->fontDescriptor = $fontDescriptor; 218 219 // Maximum size of glyf table to read in as string (otherwise reads each glyph from file) 220 $this->maxStrLenRead = 200000; 221 } 222 223 public function getMetrics($file, $fontkey, $TTCfontID = 0, $debug = false, $BMPonly = false, $useOTL = 0) 224 { 225 $this->useOTL = $useOTL; 226 $this->fontkey = $fontkey; 227 $this->filename = $file; 228 $this->fh = fopen($file, 'rb'); 229 230 if (!$this->fh) { 231 throw new \Mpdf\Exception\FontException(sprintf('Unable to open font file "%s"', $file)); 232 } 233 234 $this->_pos = 0; 235 $this->charWidths = ''; 236 $this->glyphPos = []; 237 $this->charToGlyph = []; 238 $this->tables = []; 239 $this->otables = []; 240 $this->kerninfo = []; 241 $this->haskernGPOS = []; 242 $this->hassmallcapsGSUB = []; 243 $this->ascent = 0; 244 $this->descent = 0; 245 $this->lineGap = 0; 246 $this->hheaascent = 0; 247 $this->hheadescent = 0; 248 $this->hhealineGap = 0; 249 $this->xHeight = 0; 250 $this->capHeight = 0; 251 $this->panose = []; 252 $this->sFamilyClass = 0; 253 $this->sFamilySubClass = 0; 254 $this->typoAscender = 0; 255 $this->typoDescender = 0; 256 $this->typoLineGap = 0; 257 $this->usWinAscent = 0; 258 $this->usWinDescent = 0; 259 $this->advanceWidthMax = 0; 260 $this->strikeoutSize = 0; 261 $this->strikeoutPosition = 0; 262 $this->numTTCFonts = 0; 263 $this->TTCFonts = []; 264 $this->version = $version = $this->read_ulong(); 265 $this->panose = []; 266 267 if ($version === 0x4F54544F) { 268 throw new \Mpdf\Exception\FontException(sprintf('Fonts with postscript outlines are not supported (%s)', $file)); 269 } 270 271 if ($version === 0x74746366 && !$TTCfontID) { 272 throw new \Mpdf\Exception\FontException(sprintf('TTCfontID for a TrueType Collection is not defined in mPDF "fontdata" configuration (%s)', $file)); 273 } 274 275 if (!in_array($version, [0x00010000, 0x74727565], true) && !$TTCfontID) { 276 throw new \Mpdf\Exception\FontException(sprintf('Not a TrueType font: version=%s)', $version)); 277 } 278 279 if ($TTCfontID > 0) { 280 $this->version = $version = $this->read_ulong(); // TTC Header version now 281 if (!in_array($version, [0x00010000, 0x00020000], true)) { 282 throw new \Mpdf\Exception\FontException(sprintf('Error parsing TrueType Collection: version=%s - (%s)', $version, $file)); 283 } 284 $this->numTTCFonts = $this->read_ulong(); 285 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 286 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 287 } 288 $this->seek($this->TTCFonts[$TTCfontID]['offset']); 289 $this->version = $version = $this->read_ulong(); // TTFont version again now 290 } 291 292 $this->readTableDirectory($debug); 293 $this->extractInfo($debug, $BMPonly, $useOTL); 294 295 fclose($this->fh); 296 } 297 298 function readTableDirectory($debug = false) 299 { 300 $this->numTables = $this->read_ushort(); 301 $this->searchRange = $this->read_ushort(); 302 $this->entrySelector = $this->read_ushort(); 303 $this->rangeShift = $this->read_ushort(); 304 $this->tables = []; 305 306 for ($i = 0; $i < $this->numTables; $i++) { 307 $record = []; 308 $record['tag'] = $this->read_tag(); 309 $record['checksum'] = [$this->read_ushort(), $this->read_ushort()]; 310 $record['offset'] = $this->read_ulong(); 311 $record['length'] = $this->read_ulong(); 312 $this->tables[$record['tag']] = $record; 313 } 314 315 if ($debug) { 316 $this->checksumTables(); 317 } 318 } 319 320 function checksumTables() 321 { 322 // Check the checksums for all tables 323 foreach ($this->tables as $t) { 324 if ($t['length'] > 0 && $t['length'] < $this->maxStrLenRead) { // 1.02 325 $table = $this->get_chunk($t['offset'], $t['length']); 326 $checksum = $this->calcChecksum($table); 327 if ($t['tag'] === 'head') { 328 $up = unpack('n*', substr($table, 8, 4)); 329 $adjustment[0] = $up[1]; 330 $adjustment[1] = $up[2]; 331 $checksum = $this->sub32($checksum, $adjustment); 332 } 333 $xchecksum = $t['checksum']; 334 if ($xchecksum != $checksum) { 335 throw new \Mpdf\Exception\FontException(sprintf('TTF file "%s": invalid checksum %s table: %s (expected %s)', $this->filename, dechex($checksum[0]) . dechex($checksum[1]), $t['tag'], dechex($xchecksum[0]) . dechex($xchecksum[1]))); 336 } 337 } 338 } 339 } 340 341 function sub32($x, $y) 342 { 343 $xlo = $x[1]; 344 $xhi = $x[0]; 345 $ylo = $y[1]; 346 $yhi = $y[0]; 347 348 if ($ylo > $xlo) { 349 $xlo += 1 << 16; 350 ++$yhi; 351 } 352 $reslo = $xlo - $ylo; 353 if ($yhi > $xhi) { 354 $xhi += 1 << 16; 355 } 356 $reshi = $xhi - $yhi; 357 $reshi &= 0xFFFF; 358 359 return [$reshi, $reslo]; 360 } 361 362 function calcChecksum($data) 363 { 364 if (strlen($data) % 4) { 365 $data .= str_repeat("\0", 4 - (strlen($data) % 4)); 366 } 367 368 $len = strlen($data); 369 $hi = 0x0000; 370 $lo = 0x0000; 371 372 for ($i = 0; $i < $len; $i += 4) { 373 $hi += (ord($data[$i]) << 8) + ord($data[$i + 1]); 374 $lo += (ord($data[$i + 2]) << 8) + ord($data[$i + 3]); 375 $hi += ($lo >> 16) & 0xFFFF; 376 $lo &= 0xFFFF; 377 } 378 379 $hi &= 0xFFFF; 380 381 return [$hi, $lo]; 382 } 383 384 function get_table_pos($tag) 385 { 386 if (!isset($this->tables[$tag])) { 387 return [0, 0]; 388 } 389 $offset = $this->tables[$tag]['offset']; 390 $length = $this->tables[$tag]['length']; 391 392 return [$offset, $length]; 393 } 394 395 function seek($pos) 396 { 397 $this->_pos = $pos; 398 fseek($this->fh, $this->_pos); 399 } 400 401 function skip($delta) 402 { 403 $this->_pos = $this->_pos + $delta; 404 fseek($this->fh, $delta, SEEK_CUR); 405 } 406 407 function seek_table($tag, $offset_in_table = 0) 408 { 409 $tpos = $this->get_table_pos($tag); 410 $this->_pos = $tpos[0] + $offset_in_table; 411 fseek($this->fh, $this->_pos); 412 413 return $this->_pos; 414 } 415 416 function read_tag() 417 { 418 $this->_pos += 4; 419 420 return fread($this->fh, 4); 421 } 422 423 function read_short() 424 { 425 $this->_pos += 2; 426 $s = fread($this->fh, 2); 427 $a = (ord($s[0]) << 8) + ord($s[1]); 428 if ($a & (1 << 15)) { 429 $a = ($a - (1 << 16)); 430 } 431 432 return $a; 433 } 434 435 function unpack_short($s) 436 { 437 $a = (ord($s[0]) << 8) + ord($s[1]); 438 if ($a & (1 << 15)) { 439 $a = ($a - (1 << 16)); 440 } 441 442 return $a; 443 } 444 445 function read_ushort() 446 { 447 $this->_pos += 2; 448 $s = fread($this->fh, 2); 449 450 return (ord($s[0]) << 8) + ord($s[1]); 451 } 452 453 function read_ulong() 454 { 455 $this->_pos += 4; 456 $s = fread($this->fh, 4); 457 458 // if large uInt32 as an integer, PHP converts it to -ve 459 return (ord($s[0]) * 16777216) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); // 16777216 = 1<<24 460 } 461 462 function get_ushort($pos) 463 { 464 fseek($this->fh, $pos); 465 $s = fread($this->fh, 2); 466 467 return (ord($s[0]) << 8) + ord($s[1]); 468 } 469 470 function get_ulong($pos) 471 { 472 fseek($this->fh, $pos); 473 $s = fread($this->fh, 4); 474 475 // iF large uInt32 as an integer, PHP converts it to -ve 476 return (ord($s[0]) * 16777216) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); // 16777216 = 1<<24 477 } 478 479 function pack_short($val) 480 { 481 if ($val < 0) { 482 $val = abs($val); 483 $val = ~$val; 484 ++$val; 485 } 486 487 return pack('n', $val); 488 } 489 490 function splice($stream, $offset, $value) 491 { 492 return substr($stream, 0, $offset) . $value . substr($stream, $offset + strlen($value)); 493 } 494 495 function _set_ushort($stream, $offset, $value) 496 { 497 $up = pack("n", $value); 498 499 return $this->splice($stream, $offset, $up); 500 } 501 502 function _set_short($stream, $offset, $val) 503 { 504 if ($val < 0) { 505 $val = abs($val); 506 $val = ~$val; 507 $val += 1; 508 } 509 $up = pack("n", $val); 510 511 return $this->splice($stream, $offset, $up); 512 } 513 514 function get_chunk($pos, $length) 515 { 516 fseek($this->fh, $pos); 517 if ($length < 1) { 518 return ''; 519 } 520 521 $data = (fread($this->fh, $length)); 522 523 // fix for #1504 524 // if fread is used to read from a compressed / buffered stream (e.g. phar://...) 525 // the $length parameter will be ignored - fread is limited in size (usually 8192 bytes) 526 // to fix this, the data length must be checked after reading. If the read was incomplete, 527 // try to read the rest of the data 528 $dataLen = strlen($data); 529 while ($dataLen < $length && !feof($this->fh)) { 530 $data .= fread($this->fh, $length - $dataLen); 531 $dataLen = strlen($data); 532 } 533 return $data; 534 } 535 536 function get_table($tag) 537 { 538 list($pos, $length) = $this->get_table_pos($tag); 539 if ($length == 0) { 540 return ''; 541 } 542 fseek($this->fh, $pos); 543 544 return (fread($this->fh, $length)); 545 } 546 547 function add($tag, $data) 548 { 549 if ($tag === 'head') { 550 $data = $this->splice($data, 8, "\0\0\0\0"); 551 } 552 $this->otables[$tag] = $data; 553 } 554 555 function getCTG($file, $TTCfontID = 0, $debug = false, $useOTL = false) 556 { 557 // Only called if font is not to be used as embedded subset i.e. NOT called for SIP/SMP fonts 558 $this->useOTL = $useOTL; // mPDF 5.7.1 559 $this->filename = $file; 560 $this->fh = fopen($file, 'rb'); 561 562 if (!$this->fh) { 563 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file "%s"', $file)); 564 } 565 566 $this->_pos = 0; 567 $this->charWidths = ''; 568 $this->glyphPos = []; 569 $this->charToGlyph = []; 570 $this->tables = []; 571 $this->numTTCFonts = 0; 572 $this->TTCFonts = []; 573 $this->skip(4); 574 575 if ($TTCfontID > 0) { 576 $this->version = $version = $this->read_ulong(); // TTC Header version now 577 if (!in_array($version, [0x00010000, 0x00020000], true)) { 578 throw new \Mpdf\Exception\FontException(sprintf("Error parsing TrueType Collection: version=%s (%s)", $version, $file)); 579 } 580 $this->numTTCFonts = $this->read_ulong(); 581 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 582 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 583 } 584 $this->seek($this->TTCFonts[$TTCfontID]['offset']); 585 $this->version = $version = $this->read_ulong(); // TTFont version again now 586 } 587 $this->readTableDirectory($debug); 588 589 // cmap - Character to glyph index mapping table 590 $cmap_offset = $this->seek_table('cmap'); 591 $this->skip(2); 592 $cmapTableCount = $this->read_ushort(); 593 $unicode_cmap_offset = 0; 594 for ($i = 0; $i < $cmapTableCount; $i++) { 595 $platformID = $this->read_ushort(); 596 $encodingID = $this->read_ushort(); 597 $offset = $this->read_ulong(); 598 $save_pos = $this->_pos; 599 if ($platformID == 3 && $encodingID == 1) { // Microsoft, Unicode 600 $format = $this->get_ushort($cmap_offset + $offset); 601 if ($format == 4) { 602 $unicode_cmap_offset = $cmap_offset + $offset; 603 break; 604 } 605 } elseif ($platformID == 0) { // Unicode -- assume all encodings are compatible 606 $format = $this->get_ushort($cmap_offset + $offset); 607 if ($format == 4) { 608 $unicode_cmap_offset = $cmap_offset + $offset; 609 break; 610 } 611 } 612 $this->seek($save_pos); 613 } 614 615 $glyphToChar = []; 616 $charToGlyph = []; 617 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 618 619 // Map Unmapped glyphs - from $numGlyphs 620 if ($useOTL) { 621 $this->seek_table("maxp"); 622 $this->skip(4); 623 $numGlyphs = $this->read_ushort(); 624 $bctr = 0xE000; 625 for ($gid = 1; $gid < $numGlyphs; $gid++) { 626 if (!isset($glyphToChar[$gid])) { 627 while (isset($charToGlyph[$bctr])) { 628 $bctr++; 629 } // Avoid overwriting a glyph already mapped in PUA 630 if ($bctr > 0xF8FF) { 631 throw new \Mpdf\Exception\FontException(sprintf('Font "%s" cannot map all included glyphs into Private Use Area U+E000-U+F8FF; cannot use useOTL on this font', $file)); 632 } 633 $glyphToChar[$gid][] = $bctr; 634 $charToGlyph[$bctr] = $gid; 635 $bctr++; 636 } 637 } 638 } 639 640 fclose($this->fh); 641 642 return $charToGlyph; 643 } 644 645 function getTTCFonts($file) 646 { 647 $this->filename = $file; 648 649 $this->fh = fopen($file, 'rb'); 650 if (!$this->fh) { 651 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file "%s"', $file)); 652 } 653 654 $this->numTTCFonts = 0; 655 $this->TTCFonts = []; 656 $this->version = $version = $this->read_ulong(); 657 if ($version === 0x74746366) { 658 $this->version = $version = $this->read_ulong(); // TTC Header version now 659 if (!in_array($version, [0x00010000, 0x00020000], true)) { 660 throw new \Mpdf\Exception\FontException(sprintf("Error parsing TrueType Collection: version=%s (%s)", $version, $file)); 661 } 662 } else { 663 throw new \Mpdf\Exception\FontException(sprintf("Not a TrueType Collection: version=%s (%s)", $version, $file)); 664 } 665 666 $this->numTTCFonts = $this->read_ulong(); 667 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 668 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 669 } 670 } 671 672 function extractInfo($debug = false, $BMPonly = false, $useOTL = 0) 673 { 674 // Values are all set to 0 or blank at start of getMetrics 675 // name - Naming table 676 $name_offset = $this->seek_table("name"); 677 $format = $this->read_ushort(); 678 if ($format != 0 && $format != 1) { 679 throw new \Mpdf\Exception\FontException("Error loading font: Unknown name table format $format for font $this->filename"); 680 } 681 682 $numRecords = $this->read_ushort(); 683 $string_data_offset = $name_offset + $this->read_ushort(); 684 $names = [1 => '', 2 => '', 3 => '', 4 => '', 6 => '']; 685 $K = array_keys($names); 686 $nameCount = count($names); 687 688 for ($i = 0; $i < $numRecords; $i++) { 689 690 $platformId = $this->read_ushort(); 691 $encodingId = $this->read_ushort(); 692 $languageId = $this->read_ushort(); 693 $nameId = $this->read_ushort(); 694 $length = $this->read_ushort(); 695 $offset = $this->read_ushort(); 696 697 if (!in_array($nameId, $K)) { 698 continue; 699 } 700 701 $N = ''; 702 if ($platformId == 3 && $encodingId == 1 && $languageId == 0x409) { // Microsoft, Unicode, US English, PS Name 703 $opos = $this->_pos; 704 $this->seek($string_data_offset + $offset); 705 if ($length % 2 != 0) { 706 throw new \Mpdf\Exception\FontException("Error loading font: PostScript name is UTF-16BE string of odd length for font $this->filename"); 707 } 708 $length /= 2; 709 $N = ''; 710 while ($length > 0) { 711 $char = $this->read_ushort(); 712 $N .= (chr($char)); 713 $length -= 1; 714 } 715 $this->_pos = $opos; 716 $this->seek($opos); 717 } elseif ($platformId == 1 && $encodingId == 0 && $languageId == 0) { // Macintosh, Roman, English, PS Name 718 $opos = $this->_pos; 719 $N = $this->get_chunk($string_data_offset + $offset, $length); 720 $this->_pos = $opos; 721 $this->seek($opos); 722 } 723 if ($N && $names[$nameId] == '') { 724 $names[$nameId] = $N; 725 $nameCount -= 1; 726 if ($nameCount == 0) { 727 break; 728 } 729 } 730 } 731 732 if ($names[6]) { 733 $psName = $names[6]; 734 } elseif ($names[4]) { 735 $psName = preg_replace('/ /', '-', $names[4]); 736 } elseif ($names[1]) { 737 $psName = preg_replace('/ /', '-', $names[1]); 738 } else { 739 $psName = ''; 740 } 741 742 if (!$psName) { 743 throw new \Mpdf\Exception\FontException("Error loading font: Could not find PostScript font name '$this->filename'"); 744 } 745 746 // CHECK IF psName valid (PadaukBook contains illegal characters in Name ID 6 i.e. Postscript Name) 747 $psNameInvalid = false; 748 $nameLength = strlen($psName); 749 for ($i = 0; $i < $nameLength; $i++) { 750 $c = $psName[$i]; 751 $oc = ord($c); 752 if ($oc > 126 || strpos(' [](){}<>/%', $c) !== false) { 753 //throw new \Mpdf\Exception\FontException("psName=".$psName." contains invalid character ".$c." ie U+".ord(c)); 754 $psNameInvalid = true; 755 break; 756 } 757 } 758 759 if ($psNameInvalid && $names[4]) { 760 $psName = preg_replace('/ /', '-', $names[4]); 761 } 762 763 $this->name = $psName; 764 if ($names[1]) { 765 $this->familyName = $names[1]; 766 } else { 767 $this->familyName = $psName; 768 } 769 if ($names[2]) { 770 $this->styleName = $names[2]; 771 } else { 772 $this->styleName = 'Regular'; 773 } 774 if ($names[4]) { 775 $this->fullName = $names[4]; 776 } else { 777 $this->fullName = $psName; 778 } 779 if ($names[3]) { 780 $this->uniqueFontID = $names[3]; 781 } else { 782 $this->uniqueFontID = $psName; 783 } 784 785 if (!$psNameInvalid && $names[6]) { 786 $this->fullName = $names[6]; 787 } 788 789 // head - Font header table 790 $this->seek_table('head'); 791 if ($debug) { 792 $ver_maj = $this->read_ushort(); 793 $ver_min = $this->read_ushort(); 794 if ($ver_maj != 1) { 795 throw new \Mpdf\Exception\FontException('Error loading font: Unknown head table version ' . $ver_maj . '.' . $ver_min); 796 } 797 $this->fontRevision = $this->read_ushort() . $this->read_ushort(); 798 799 $this->skip(4); 800 $magic = $this->read_ulong(); 801 if ($magic !== 0x5F0F3CF5) { 802 throw new \Mpdf\Exception\FontException('Error loading font: Invalid head table magic ' . $magic); 803 } 804 $this->skip(2); 805 } else { 806 $this->skip(18); 807 } 808 $this->unitsPerEm = $unitsPerEm = $this->read_ushort(); 809 $scale = 1000 / $unitsPerEm; 810 $this->skip(16); 811 $xMin = $this->read_short(); 812 $yMin = $this->read_short(); 813 $xMax = $this->read_short(); 814 $yMax = $this->read_short(); 815 $this->bbox = [($xMin * $scale), ($yMin * $scale), ($xMax * $scale), ($yMax * $scale)]; 816 817 $this->skip(3 * 2); 818 $indexToLocFormat = $this->read_ushort(); 819 $glyphDataFormat = $this->read_ushort(); 820 if ($glyphDataFormat != 0) { 821 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown glyph data format %s', $glyphDataFormat)); 822 } 823 824 // hhea metrics table 825 if (isset($this->tables["hhea"])) { 826 $this->seek_table("hhea"); 827 $this->skip(4); 828 $hheaAscender = $this->read_short(); 829 $hheaDescender = $this->read_short(); 830 $hheaLineGap = $this->read_short(); 831 $hheaAdvanceWidthMax = $this->read_ushort(); 832 $this->hheaascent = ($hheaAscender * $scale); 833 $this->hheadescent = ($hheaDescender * $scale); 834 $this->hhealineGap = ($hheaLineGap * $scale); 835 $this->advanceWidthMax = ($hheaAdvanceWidthMax * $scale); 836 } 837 838 // OS/2 - OS/2 and Windows metrics table 839 $use_typo_metrics = false; 840 if (isset($this->tables["OS/2"])) { 841 $this->seek_table("OS/2"); 842 $version = $this->read_ushort(); 843 $this->skip(2); 844 $usWeightClass = $this->read_ushort(); 845 $this->skip(2); 846 $fsType = $this->read_ushort(); 847 if ($fsType == 0x0002 || ($fsType & 0x0300) != 0) { 848 $this->restrictedUse = true; 849 } 850 851 $this->skip(16); 852 $yStrikeoutSize = $this->read_short(); 853 $yStrikeoutPosition = $this->read_short(); 854 $this->strikeoutSize = ($yStrikeoutSize * $scale); 855 $this->strikeoutPosition = ($yStrikeoutPosition * $scale); 856 857 $sF = $this->read_short(); 858 $this->sFamilyClass = ($sF >> 8); 859 $this->sFamilySubClass = ($sF & 0xFF); 860 $this->_pos += 10; //PANOSE = 10 byte length 861 $panose = fread($this->fh, 10); 862 $this->panose = []; 863 $panoseLenght = strlen($panose); 864 for ($p = 0; $p < $panoseLenght; $p++) { 865 $this->panose[] = ord($panose[$p]); 866 } 867 868 $this->skip(20); 869 $fsSelection = $this->read_ushort(); 870 $use_typo_metrics = (($fsSelection & 0x80) === 0x80); // bit#7 = USE_TYPO_METRICS 871 $this->skip(4); 872 873 $sTypoAscender = $this->read_short(); 874 $sTypoDescender = $this->read_short(); 875 $sTypoLineGap = $this->read_short(); 876 877 if ($sTypoAscender) { 878 $this->typoAscender = ($sTypoAscender * $scale); 879 } 880 if ($sTypoDescender) { 881 $this->typoDescender = ($sTypoDescender * $scale); 882 } 883 if ($sTypoLineGap) { 884 $this->typoLineGap = ($sTypoLineGap * $scale); 885 } 886 887 $usWinAscent = $this->read_ushort(); 888 $usWinDescent = $this->read_ushort(); 889 if ($usWinAscent) { 890 $this->usWinAscent = ($usWinAscent * $scale); 891 } 892 if ($usWinDescent) { 893 $this->usWinDescent = ($usWinDescent * $scale); 894 } 895 896 if ($version > 1) { 897 $this->skip(8); 898 $sxHeight = $this->read_short(); 899 $this->xHeight = ($sxHeight * $scale); 900 $sCapHeight = $this->read_short(); 901 $this->capHeight = ($sCapHeight * $scale); 902 } 903 } else { 904 $usWeightClass = 400; 905 } 906 $this->stemV = 50 + (int) (($usWeightClass / 65.0) ** 2); 907 908 // FONT DESCRIPTOR METRICS 909 if ($this->fontDescriptor === 'winTypo') { 910 $this->ascent = $this->typoAscender; 911 $this->descent = $this->typoDescender; 912 $this->lineGap = $this->typoLineGap; 913 } elseif ($this->fontDescriptor === 'mac') { 914 $this->ascent = $this->hheaascent; 915 $this->descent = $this->hheadescent; 916 $this->lineGap = $this->hhealineGap; 917 } else { // $this->fontDescriptor === 'win' 918 $this->ascent = $this->usWinAscent; 919 $this->descent = -$this->usWinDescent; 920 $this->lineGap = 0; 921 922 // Special case - if either the winAscent or winDescent are greater than the 923 // font bounding box yMin yMax, then reduce them accordingly. 924 // This works with Myanmar Text (Windows 8 version) to give a 925 // line-height normal that is equivalent to that produced in browsers. 926 // Also Khmer OS = compatible with MSWord, Wordpad and browser. 927 if ($this->ascent > $this->bbox[3]) { 928 $this->ascent = $this->bbox[3]; 929 } 930 931 if ($this->descent < $this->bbox[1]) { 932 $this->descent = $this->bbox[1]; 933 } 934 935 // Override case - if the USE_TYPO_METRICS bit is set on OS/2 fsSelection 936 // this is telling the font to use the sTypo values and not the usWinAscent values. 937 // This works as a fix with Cambria Math to give a normal line-height; 938 // at present, this is the only font I have found with this bit set; 939 // although note that MS WordPad and windows FF browser uses the big line-height from winAscent 940 // but Word 2007 get it right 941 if ($use_typo_metrics && $this->typoAscender) { 942 $this->ascent = $this->typoAscender; 943 $this->descent = $this->typoDescender; 944 $this->lineGap = $this->typoLineGap; 945 } 946 } 947 948 // post - PostScript table 949 $this->seek_table('post'); 950 if ($debug) { 951 $ver_maj = $this->read_ushort(); 952 if ($ver_maj < 1 || $ver_maj > 4) { 953 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown post table version %s', $ver_maj)); 954 } 955 } else { 956 $this->skip(4); 957 } 958 959 $this->italicAngle = $this->read_short() + $this->read_ushort() / 65536.0; 960 $this->underlinePosition = $this->read_short() * $scale; 961 $this->underlineThickness = $this->read_short() * $scale; 962 $isFixedPitch = $this->read_ulong(); 963 964 $this->flags = 4; 965 966 if ($this->italicAngle != 0) { 967 $this->flags |= 64; 968 } 969 if ($usWeightClass >= 600) { 970 $this->flags |= 262144; 971 } 972 if ($isFixedPitch) { 973 $this->flags |= 1; 974 } 975 976 // hhea - Horizontal header table 977 $this->seek_table('hhea'); 978 if ($debug) { 979 $ver_maj = $this->read_ushort(); 980 if ($ver_maj != 1) { 981 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown hhea table version %s', $ver_maj)); 982 } 983 $this->skip(28); 984 } else { 985 $this->skip(32); 986 } 987 988 $metricDataFormat = $this->read_ushort(); 989 990 if ($metricDataFormat != 0) { 991 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown horizontal metric data format "%s"', $metricDataFormat)); 992 } 993 994 $numberOfHMetrics = $this->read_ushort(); 995 996 if ($numberOfHMetrics == 0) { 997 throw new \Mpdf\Exception\FontException('Error loading font: Number of horizontal metrics is 0'); 998 } 999 1000 // maxp - Maximum profile table 1001 $this->seek_table('maxp'); 1002 if ($debug) { 1003 $ver_maj = $this->read_ushort(); 1004 if ($ver_maj != 1) { 1005 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown maxp table version %s', $ver_maj)); 1006 } 1007 } else { 1008 $this->skip(4); 1009 } 1010 $numGlyphs = $this->read_ushort(); 1011 1012 // cmap - Character to glyph index mapping table 1013 $cmap_offset = $this->seek_table('cmap'); 1014 $this->skip(2); 1015 $cmapTableCount = $this->read_ushort(); 1016 $unicode_cmap_offset = 0; 1017 for ($i = 0; $i < $cmapTableCount; $i++) { 1018 $platformID = $this->read_ushort(); 1019 $encodingID = $this->read_ushort(); 1020 $offset = $this->read_ulong(); 1021 $save_pos = $this->_pos; 1022 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode 1023 $format = $this->get_ushort($cmap_offset + $offset); 1024 if ($format == 4) { 1025 if (!$unicode_cmap_offset) { 1026 $unicode_cmap_offset = $cmap_offset + $offset; 1027 } 1028 if ($BMPonly) { 1029 break; 1030 } 1031 } 1032 } elseif ((($platformID == 3 && $encodingID == 10) || $platformID == 0) && !$BMPonly) { // Microsoft, Unicode Format 12 table HKCS 1033 $format = $this->get_ushort($cmap_offset + $offset); 1034 if ($format == 12) { 1035 $unicode_cmap_offset = $cmap_offset + $offset; 1036 break; 1037 } 1038 } 1039 $this->seek($save_pos); 1040 } 1041 1042 if (!$unicode_cmap_offset) { 1043 throw new \Mpdf\Exception\FontException(sprintf('Font "%s" does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)', $this->filename)); 1044 } 1045 1046 $sipset = false; 1047 $smpset = false; 1048 1049 $this->rtlPUAstr = ''; 1050 $this->GSUBScriptLang = []; 1051 $this->GSUBFeatures = []; 1052 $this->GSUBLookups = []; 1053 $this->GPOSScriptLang = []; 1054 $this->GPOSFeatures = []; 1055 $this->GPOSLookups = []; 1056 $this->glyphIDtoUni = ''; 1057 1058 // Format 12 CMAP does characters above Unicode BMP i.e. some HKCS characters U+20000 and above 1059 if ($format == 12 && !$BMPonly) { 1060 $this->maxUniChar = 0; 1061 $this->seek($unicode_cmap_offset + 4); 1062 $length = $this->read_ulong(); 1063 $limit = $unicode_cmap_offset + $length; 1064 $this->skip(4); 1065 1066 $nGroups = $this->read_ulong(); 1067 1068 $glyphToChar = []; 1069 $charToGlyph = []; 1070 for ($i = 0; $i < $nGroups; $i++) { 1071 $startCharCode = $this->read_ulong(); 1072 $endCharCode = $this->read_ulong(); 1073 $startGlyphCode = $this->read_ulong(); 1074 // ZZZ98 1075 if ($endCharCode > 0x20000 && $endCharCode < 0x2FFFF) { 1076 $sipset = true; 1077 } elseif ($endCharCode > 0x10000 && $endCharCode < 0x1FFFF) { 1078 $smpset = true; 1079 } 1080 $offset = 0; 1081 for ($unichar = $startCharCode; $unichar <= $endCharCode; $unichar++) { 1082 $glyph = $startGlyphCode + $offset; 1083 $offset++; 1084 // ZZZ98 1085 if ($unichar < 0x30000) { 1086 $charToGlyph[$unichar] = $glyph; 1087 $this->maxUniChar = max($unichar, $this->maxUniChar); 1088 $glyphToChar[$glyph][] = $unichar; 1089 } 1090 } 1091 } 1092 } else { 1093 $glyphToChar = []; 1094 $charToGlyph = []; 1095 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 1096 } 1097 $this->sipset = $sipset; 1098 $this->smpset = $smpset; 1099 1100 // Map Unmapped glyphs (or glyphs mapped to upper PUA U+F00000 onwards i.e. > U+2FFFF) - from $numGlyphs 1101 if ($this->useOTL) { 1102 1103 $bctr = 0xE000; 1104 1105 for ($gid = 1; $gid < $numGlyphs; $gid++) { 1106 1107 if (!isset($glyphToChar[$gid])) { 1108 1109 while (isset($charToGlyph[$bctr])) { 1110 $bctr++; 1111 } 1112 1113 // Avoid overwriting a glyph already mapped in PUA 1114 // ZZZ98 1115 if (($bctr > 0xF8FF) && ($bctr < 0x2CEB0)) { 1116 if (!$BMPonly) { 1117 $bctr = 0x2CEB0; // Use unassigned area 0x2CEB0 to 0x2F7FF (space for 10,000 characters) 1118 $this->sipset = $sipset = true; // forces subsetting; also ensure charwidths are saved 1119 while (isset($charToGlyph[$bctr])) { 1120 $bctr++; 1121 } 1122 } else { 1123 throw new \Mpdf\Exception\FontException(sprintf('The font "%s" does not have enough space to map all (unmapped) included glyphs into Private Use Area U+E000-U+F8FF', $names[1])); 1124 } 1125 } 1126 1127 $glyphToChar[$gid][] = $bctr; 1128 $charToGlyph[$bctr] = $gid; 1129 $this->maxUniChar = max($bctr, $this->maxUniChar); 1130 $bctr++; 1131 } 1132 } 1133 } 1134 1135 $this->glyphToChar = $glyphToChar; 1136 1137 $this->GSUBScriptLang = []; 1138 $this->rtlPUAstr = ''; 1139 if ($useOTL) { 1140 $this->_getGDEFtables(); 1141 list($this->GSUBScriptLang, $this->GSUBFeatures, $this->GSUBLookups, $this->rtlPUAstr) = $this->_getGSUBtables(); 1142 list($this->GPOSScriptLang, $this->GPOSFeatures, $this->GPOSLookups) = $this->_getGPOStables(); 1143 $this->glyphIDtoUni = str_pad('', 256 * 256 * 3, "\x00"); 1144 foreach ($glyphToChar as $gid => $arr) { 1145 if (isset($glyphToChar[$gid][0])) { 1146 $char = $glyphToChar[$gid][0]; 1147 1148 if ($char != 0 && $char != 65535) { 1149 $this->glyphIDtoUni[$gid * 3] = chr($char >> 16); 1150 $this->glyphIDtoUni[$gid * 3 + 1] = chr(($char >> 8) & 0xFF); 1151 $this->glyphIDtoUni[$gid * 3 + 2] = chr($char & 0xFF); 1152 } 1153 } 1154 } 1155 } 1156 1157 // if xHeight and/or CapHeight are not available from OS/2 (e.g. eraly versions) 1158 // Calculate from yMax of 'x' or 'H' Glyphs... 1159 if ($this->xHeight == 0) { 1160 if (isset($charToGlyph[0x78])) { 1161 $gidx = $charToGlyph[0x78]; // U+0078 (LATIN SMALL LETTER X) 1162 $start = $this->seek_table('loca'); 1163 if ($indexToLocFormat == 0) { 1164 $this->skip($gidx * 2); 1165 $locax = $this->read_ushort() * 2; 1166 } elseif ($indexToLocFormat == 1) { 1167 $this->skip($gidx * 4); 1168 $locax = $this->read_ulong(); 1169 } 1170 $start = $this->seek_table('glyf'); 1171 $this->skip($locax); 1172 $this->skip(8); 1173 $yMaxx = $this->read_short(); 1174 $this->xHeight = $yMaxx * $scale; 1175 } 1176 } 1177 1178 if ($this->capHeight == 0) { 1179 if (isset($charToGlyph[0x48])) { 1180 $gidH = $charToGlyph[0x48]; // U+0048 (LATIN CAPITAL LETTER H) 1181 $start = $this->seek_table('loca'); 1182 if ($indexToLocFormat == 0) { 1183 $this->skip($gidH * 2); 1184 $locaH = $this->read_ushort() * 2; 1185 } elseif ($indexToLocFormat == 1) { 1186 $this->skip($gidH * 4); 1187 $locaH = $this->read_ulong(); 1188 } 1189 $start = $this->seek_table('glyf'); 1190 $this->skip($locaH); 1191 $this->skip(8); 1192 $yMaxH = $this->read_short(); 1193 $this->capHeight = $yMaxH * $scale; 1194 } else { 1195 $this->capHeight = $this->ascent; 1196 } 1197 // final default is to set it = to Ascent 1198 } 1199 1200 // hmtx - Horizontal metrics table 1201 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale); 1202 1203 // kern - Kerning pair table 1204 // Recognises old form of Kerning table - as required by Windows - Format 0 only 1205 $kern_offset = $this->seek_table("kern"); 1206 $version = $this->read_ushort(); 1207 $nTables = $this->read_ushort(); 1208 1209 // subtable header 1210 $sversion = $this->read_ushort(); 1211 $slength = $this->read_ushort(); 1212 $scoverage = $this->read_ushort(); 1213 $format = $scoverage >> 8; 1214 if ($kern_offset && $version == 0 && $format == 0) { 1215 // Format 0 1216 $nPairs = $this->read_ushort(); 1217 $this->skip(6); 1218 for ($i = 0; $i < $nPairs; $i++) { 1219 $left = $this->read_ushort(); 1220 $right = $this->read_ushort(); 1221 $val = $this->read_short(); 1222 if (isset($glyphToChar[$left]) && count($glyphToChar[$left]) == 1 && isset($glyphToChar[$right]) && count($glyphToChar[$right]) == 1) { 1223 if ($left != 32 && $right != 32) { 1224 $this->kerninfo[$glyphToChar[$left][0]][$glyphToChar[$right][0]] = intval($val * $scale); 1225 } 1226 } 1227 } 1228 } 1229 } 1230 1231 function _getGDEFtables() 1232 { 1233 // http://www.microsoft.com/typography/otspec/gdef.htm 1234 if (isset($this->tables["GDEF"])) { 1235 $gdef_offset = $this->seek_table("GDEF"); 1236 1237 // ULONG Version of the GDEF table-currently 0x00010000 1238 $ver_maj = $this->read_ushort(); 1239 $ver_min = $this->read_ushort(); 1240 $GlyphClassDef_offset = $this->read_ushort(); 1241 $AttachList_offset = $this->read_ushort(); 1242 $LigCaretList_offset = $this->read_ushort(); 1243 $MarkAttachClassDef_offset = $this->read_ushort(); 1244 1245 // Version 0x00010002 of GDEF header contains additional Offset to a list defining mark glyph set definitions (MarkGlyphSetDef) 1246 if ($ver_min == 2) { 1247 $MarkGlyphSetsDef_offset = $this->read_ushort(); 1248 } 1249 1250 // GlyphClassDef 1251 if ($GlyphClassDef_offset) { 1252 1253 $this->seek($gdef_offset + $GlyphClassDef_offset); 1254 // 1 Base glyph (single character, spacing glyph) 1255 // 2 Ligature glyph (multiple character, spacing glyph) 1256 // 3 Mark glyph (non-spacing combining glyph) 1257 // 4 Component glyph (part of single character, spacing glyph) 1258 $GlyphByClass = $this->_getClassDefinitionTable(); 1259 } else { 1260 $GlyphByClass = []; 1261 } 1262 1263 if (isset($GlyphByClass[1]) && count($GlyphByClass[1]) > 0) { 1264 $this->GlyphClassBases = ' ' . implode('| ', $GlyphByClass[1]); 1265 } else { 1266 $this->GlyphClassBases = ''; 1267 } 1268 if (isset($GlyphByClass[2]) && count($GlyphByClass[2]) > 0) { 1269 $this->GlyphClassLigatures = ' ' . implode('| ', $GlyphByClass[2]); 1270 } else { 1271 $this->GlyphClassLigatures = ''; 1272 } 1273 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) { 1274 $this->GlyphClassMarks = ' ' . implode('| ', $GlyphByClass[3]); 1275 } else { 1276 $this->GlyphClassMarks = ''; 1277 } 1278 if (isset($GlyphByClass[4]) && count($GlyphByClass[4]) > 0) { 1279 $this->GlyphClassComponents = ' ' . implode('| ', $GlyphByClass[4]); 1280 } else { 1281 $this->GlyphClassComponents = ''; 1282 } 1283 1284 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) { 1285 $Marks = $GlyphByClass[3]; 1286 } else { // to use for MarkAttachmentType 1287 $Marks = []; 1288 } 1289 1290 /* Required for GPOS 1291 // Attachment List 1292 if ($AttachList_offset) { 1293 $this->seek($gdef_offset+$AttachList_offset ); 1294 } 1295 The Attachment Point List table (AttachmentList) identifies all the attachment points defined in the GPOS table and their associated glyphs so a client can quickly access coordinates for each glyph's attachment points. As a result, the client can cache coordinates for attachment points along with glyph bitmaps and avoid recalculating the attachment points each time it displays a glyph. Without this table, processing speed would be slower because the client would have to decode the GPOS lookups that define attachment points and compile the points in a list. 1296 1297 The Attachment List table (AttachList) may be used to cache attachment point coordinates along with glyph bitmaps. 1298 1299 The table consists of an offset to a Coverage table (Coverage) listing all glyphs that define attachment points in the GPOS table, a count of the glyphs with attachment points (GlyphCount), and an array of offsets to AttachPoint tables (AttachPoint). The array lists the AttachPoint tables, one for each glyph in the Coverage table, in the same order as the Coverage Index. 1300 AttachList table 1301 Type Name Description 1302 Offset Coverage Offset to Coverage table - from beginning of AttachList table 1303 uint16 GlyphCount Number of glyphs with attachment points 1304 Offset AttachPoint[GlyphCount] Array of offsets to AttachPoint tables-from beginning of AttachList table-in Coverage Index order 1305 1306 An AttachPoint table consists of a count of the attachment points on a single glyph (PointCount) and an array of contour indices of those points (PointIndex), listed in increasing numerical order. 1307 1308 AttachPoint table 1309 Type Name Description 1310 uint16 PointCount Number of attachment points on this glyph 1311 uint16 PointIndex[PointCount] Array of contour point indices -in increasing numerical order 1312 1313 See Example 3 - http://www.microsoft.com/typography/otspec/gdef.htm 1314 */ 1315 1316 // Ligature Caret List 1317 // The Ligature Caret List table (LigCaretList) defines caret positions for all the ligatures in a font. 1318 // Not required for mDPF 1319 // MarkAttachmentType 1320 if ($MarkAttachClassDef_offset) { 1321 $this->seek($gdef_offset + $MarkAttachClassDef_offset); 1322 $MarkAttachmentTypes = $this->_getClassDefinitionTable(); 1323 foreach ($MarkAttachmentTypes as $class => $glyphs) { 1324 if (is_array($Marks) && count($Marks)) { 1325 $mat = array_diff($Marks, $MarkAttachmentTypes[$class]); 1326 sort($mat, SORT_STRING); 1327 } else { 1328 $mat = []; 1329 } 1330 1331 $this->MarkAttachmentType[$class] = ' ' . implode('| ', $mat); 1332 } 1333 } else { 1334 $this->MarkAttachmentType = []; 1335 } 1336 1337 // MarkGlyphSets only in Version 0x00010002 of GDEF 1338 if ($ver_min == 2 && $MarkGlyphSetsDef_offset) { 1339 $this->seek($gdef_offset + $MarkGlyphSetsDef_offset); 1340 $MarkSetTableFormat = $this->read_ushort(); 1341 $MarkSetCount = $this->read_ushort(); 1342 $MarkSetOffset = []; 1343 for ($i = 0; $i < $MarkSetCount; $i++) { 1344 $MarkSetOffset[] = $this->read_ulong(); 1345 } 1346 for ($i = 0; $i < $MarkSetCount; $i++) { 1347 $this->seek($MarkSetOffset[$i]); 1348 $glyphs = $this->_getCoverage(); 1349 $this->MarkGlyphSets[$i] = ' ' . implode('| ', $glyphs); 1350 } 1351 } else { 1352 $this->MarkGlyphSets = []; 1353 } 1354 } else { 1355 throw new \Mpdf\Exception\FontException(sprintf('Unable to set font "%s" to use OTL as it does not include OTL tables (or at least not a GDEF table).', $this->filename)); 1356 } 1357 1358 $GSUB_offset = 0; 1359 $GPOS_offset = 0; 1360 $GSUB_length = 0; 1361 1362 $s = ''; 1363 1364 if (isset($this->tables['GSUB'])) { 1365 $GSUB_offset = $this->seek_table('GSUB'); 1366 $GSUB_length = $this->tables['GSUB']['length']; 1367 $s .= fread($this->fh, $this->tables['GSUB']['length']); 1368 } 1369 1370 if (isset($this->tables['GPOS'])) { 1371 $GPOS_offset = $this->seek_table('GPOS'); 1372 $s .= fread($this->fh, $this->tables['GPOS']['length']); 1373 } 1374 1375 if ($s) { 1376 $this->fontCache->write($this->fontkey . '.GSUBGPOStables.dat', $s); 1377 } 1378 1379 $font = [ 1380 'GSUB_offset' => $GSUB_offset, 1381 'GPOS_offset' => $GPOS_offset, 1382 'GSUB_length' => $GSUB_length, 1383 'GlyphClassBases' => $this->GlyphClassBases, 1384 'GlyphClassMarks' => $this->GlyphClassMarks, 1385 'GlyphClassLigatures' => $this->GlyphClassLigatures, 1386 'GlyphClassComponents' => $this->GlyphClassComponents, 1387 'MarkGlyphSets' => $this->MarkGlyphSets, 1388 'MarkAttachmentType' => $this->MarkAttachmentType, 1389 ]; 1390 1391 $this->fontCache->jsonWrite($this->fontkey . '.GDEFdata.json', $font); 1392 } 1393 1394 function _getClassDefinitionTable() 1395 { 1396 // NB Any glyph not included in the range of covered GlyphIDs automatically belongs to Class 0. This is not returned by this function 1397 $ClassFormat = $this->read_ushort(); 1398 $GlyphByClass = []; 1399 1400 if ($ClassFormat == 1) { 1401 $StartGlyph = $this->read_ushort(); 1402 $GlyphCount = $this->read_ushort(); 1403 for ($i = 0; $i < $GlyphCount; $i++) { 1404 $gid = $StartGlyph + $i; 1405 $class = $this->read_ushort(); 1406 // Several fonts (mainly dejavu.../Freeserif etc) have a MarkAttachClassDef Format 1, where StartGlyph is 0 and GlyphCount is 1 1407 // This doesn't seem to do anything useful? 1408 // Freeserif does not have $this->glyphToChar[0] allocated and would throw an error, so check if isset: 1409 if (isset($this->glyphToChar[$gid][0])) { 1410 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]); 1411 } 1412 } 1413 } elseif ($ClassFormat == 2) { 1414 $tableCount = $this->read_ushort(); 1415 for ($i = 0; $i < $tableCount; $i++) { 1416 $startGlyphID = $this->read_ushort(); 1417 $endGlyphID = $this->read_ushort(); 1418 $class = $this->read_ushort(); 1419 for ($gid = $startGlyphID; $gid <= $endGlyphID; $gid++) { 1420 if (isset($this->glyphToChar[$gid][0])) { 1421 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]); 1422 } 1423 } 1424 } 1425 } 1426 foreach ($GlyphByClass as $class => $glyphs) { 1427 sort($GlyphByClass[$class], SORT_STRING); // SORT makes it easier to read in development ? order not important ??? 1428 } 1429 ksort($GlyphByClass); 1430 1431 return $GlyphByClass; 1432 } 1433 1434 /** 1435 * GSUB - Glyph Substitution 1436 */ 1437 function _getGSUBtables() 1438 { 1439 if (!isset($this->tables['GSUB'])) { 1440 return [[], [], [], '']; 1441 } 1442 1443 $ffeats = []; 1444 $gsub_offset = $this->seek_table('GSUB'); 1445 $this->skip(4); 1446 $ScriptList_offset = $gsub_offset + $this->read_ushort(); 1447 $FeatureList_offset = $gsub_offset + $this->read_ushort(); 1448 $LookupList_offset = $gsub_offset + $this->read_ushort(); 1449 1450 // ScriptList 1451 $this->seek($ScriptList_offset); 1452 $ScriptCount = $this->read_ushort(); 1453 for ($i = 0; $i < $ScriptCount; $i++) { 1454 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc. 1455 $ScriptTableOffset = $this->read_ushort(); 1456 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset; 1457 } 1458 1459 // Script Table 1460 foreach ($ffeats as $t => $o) { 1461 $ls = []; 1462 $this->seek($o); 1463 $DefLangSys_offset = $this->read_ushort(); 1464 if ($DefLangSys_offset > 0) { 1465 $ls['DFLT'] = $DefLangSys_offset + $o; 1466 } 1467 $LangSysCount = $this->read_ushort(); 1468 for ($i = 0; $i < $LangSysCount; $i++) { 1469 $LangTag = $this->read_tag(); // = 1470 $LangTableOffset = $this->read_ushort(); 1471 $ls[$LangTag] = $o + $LangTableOffset; 1472 } 1473 $ffeats[$t] = $ls; 1474 } 1475 1476 // Get FeatureIndexList 1477 // LangSys Table - from first listed langsys 1478 foreach ($ffeats as $st => $scripts) { 1479 foreach ($scripts as $t => $o) { 1480 $FeatureIndex = []; 1481 $langsystable_offset = $o; 1482 $this->seek($langsystable_offset); 1483 $LookUpOrder = $this->read_ushort(); //==NULL 1484 $ReqFeatureIndex = $this->read_ushort(); 1485 if ($ReqFeatureIndex != 0xFFFF) { 1486 $FeatureIndex[] = $ReqFeatureIndex; 1487 } 1488 $FeatureCount = $this->read_ushort(); 1489 for ($i = 0; $i < $FeatureCount; $i++) { 1490 $FeatureIndex[] = $this->read_ushort(); // = index of feature 1491 } 1492 $ffeats[$st][$t] = $FeatureIndex; 1493 } 1494 } 1495 1496 // Feauture List => LookupListIndex es 1497 $this->seek($FeatureList_offset); 1498 $FeatureCount = $this->read_ushort(); 1499 $Feature = []; 1500 1501 for ($i = 0; $i < $FeatureCount; $i++) { 1502 $tag = $this->read_tag(); 1503 if ($tag == 'smcp') { 1504 $this->hassmallcapsGSUB = true; 1505 } 1506 $Feature[$i] = ['tag' => $tag]; 1507 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort(); 1508 } 1509 1510 for ($i = 0; $i < $FeatureCount; $i++) { 1511 $this->seek($Feature[$i]['offset']); 1512 $this->read_ushort(); // null [FeatureParams] 1513 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort(); 1514 $Feature[$i]['LookupListIndex'] = []; 1515 for ($c = 0; $c < $Lookupcount; $c++) { 1516 $Feature[$i]['LookupListIndex'][] = $this->read_ushort(); 1517 } 1518 } 1519 1520 foreach ($ffeats as $st => $scripts) { 1521 foreach ($scripts as $t => $o) { 1522 $FeatureIndex = $ffeats[$st][$t]; 1523 foreach ($FeatureIndex as $k => $fi) { 1524 $ffeats[$st][$t][$k] = $Feature[$fi]; 1525 } 1526 } 1527 } 1528 1529 $gsub = []; 1530 $GSUBScriptLang = []; 1531 foreach ($ffeats as $st => $scripts) { 1532 foreach ($scripts as $t => $langsys) { 1533 $lg = []; 1534 foreach ($langsys as $ft) { 1535 $lg[$ft['LookupListIndex'][0]] = $ft; 1536 } 1537 // list of Lookups in order they need to be run i.e. order listed in Lookup table 1538 ksort($lg); 1539 foreach ($lg as $ft) { 1540 $gsub[$st][$t][$ft['tag']] = $ft['LookupListIndex']; 1541 } 1542 if (!isset($GSUBScriptLang[$st])) { 1543 $GSUBScriptLang[$st] = ''; 1544 } 1545 $GSUBScriptLang[$st] .= $t . ' '; 1546 } 1547 } 1548 1549 // Get metadata and offsets for whole Lookup List table 1550 $this->seek($LookupList_offset); 1551 $LookupCount = $this->read_ushort(); 1552 $GSLookup = []; 1553 $Offsets = []; 1554 $SubtableCount = []; 1555 1556 for ($i = 0; $i < $LookupCount; $i++) { 1557 $Offsets[$i] = $LookupList_offset + $this->read_ushort(); 1558 } 1559 1560 for ($i = 0; $i < $LookupCount; $i++) { 1561 1562 $this->seek($Offsets[$i]); 1563 1564 $GSLookup[$i]['Type'] = $this->read_ushort(); 1565 $GSLookup[$i]['Flag'] = $flag = $this->read_ushort(); 1566 $GSLookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort(); 1567 1568 for ($c = 0; $c < $SubtableCount[$i]; $c++) { 1569 $GSLookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort(); 1570 } 1571 1572 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure 1573 if (($flag & 0x0010) == 0x0010) { 1574 $GSLookup[$i]['MarkFilteringSet'] = $this->read_ushort(); 1575 } else { 1576 $GSLookup[$i]['MarkFilteringSet'] = ''; 1577 } 1578 1579 // Lookup Type 7: Extension 1580 if ($GSLookup[$i]['Type'] == 7) { 1581 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type 1582 for ($c = 0; $c < $SubtableCount[$i]; $c++) { 1583 $this->seek($GSLookup[$i]['Subtables'][$c]); 1584 $ExtensionPosFormat = $this->read_ushort(); 1585 $type = $this->read_ushort(); 1586 $ext_offset = $this->read_ulong(); 1587 $GSLookup[$i]['Subtables'][$c] = $GSLookup[$i]['Subtables'][$c] + $ext_offset; 1588 } 1589 $GSLookup[$i]['Type'] = $type; 1590 } 1591 } 1592 1593 // Process Whole LookupList - Get LuCoverage = Lookup coverage just for first glyph 1594 $this->GSLuCoverage = []; 1595 for ($i = 0; $i < $LookupCount; $i++) { 1596 for ($c = 0; $c < $GSLookup[$i]['SubtableCount']; $c++) { 1597 $this->seek($GSLookup[$i]['Subtables'][$c]); 1598 $PosFormat = $this->read_ushort(); 1599 1600 if ($GSLookup[$i]['Type'] == 5 && $PosFormat == 3) { 1601 $this->skip(4); 1602 } elseif ($GSLookup[$i]['Type'] == 6 && $PosFormat == 3) { 1603 $BacktrackGlyphCount = $this->read_ushort(); 1604 $this->skip(2 * $BacktrackGlyphCount + 2); 1605 } 1606 1607 // NB Coverage only looks at glyphs for position 1 (i.e. 5.3 and 6.3) // NEEDS TO READ ALL ******************** 1608 $Coverage = $GSLookup[$i]['Subtables'][$c] + $this->read_ushort(); 1609 $this->seek($Coverage); 1610 $glyphs = $this->_getCoverage(false, 2); 1611 $this->GSLuCoverage[$i][$c] = $glyphs; 1612 } 1613 } 1614 1615 // $this->GSLuCoverage and $GSLookup 1616 $this->fontCache->jsonWrite($this->fontkey . '.GSUBdata.json', $this->GSLuCoverage); 1617 1618 // Now repeats as original to get Substitution rules 1619 // Get metadata and offsets for whole Lookup List table 1620 $this->seek($LookupList_offset); 1621 $LookupCount = $this->read_ushort(); 1622 $Lookup = []; 1623 1624 for ($i = 0; $i < $LookupCount; $i++) { 1625 $Lookup[$i]['offset'] = $LookupList_offset + $this->read_ushort(); 1626 } 1627 1628 for ($i = 0; $i < $LookupCount; $i++) { 1629 $this->seek($Lookup[$i]['offset']); 1630 $Lookup[$i]['Type'] = $this->read_ushort(); 1631 $Lookup[$i]['Flag'] = $flag = $this->read_ushort(); 1632 $Lookup[$i]['SubtableCount'] = $this->read_ushort(); 1633 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1634 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['offset'] + $this->read_ushort(); 1635 } 1636 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure 1637 if (($flag & 0x0010) == 0x0010) { 1638 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort(); 1639 } else { 1640 $Lookup[$i]['MarkFilteringSet'] = ''; 1641 } 1642 1643 // Lookup Type 7: Extension 1644 if ($Lookup[$i]['Type'] == 7) { 1645 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type 1646 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1647 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']); 1648 $ExtensionPosFormat = $this->read_ushort(); 1649 $type = $this->read_ushort(); 1650 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ulong(); 1651 } 1652 $Lookup[$i]['Type'] = $type; 1653 } 1654 } 1655 1656 // Process (1) Whole LookupList 1657 for ($i = 0; $i < $LookupCount; $i++) { 1658 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1659 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']); 1660 $SubstFormat = $this->read_ushort(); 1661 $Lookup[$i]['Subtable'][$c]['Format'] = $SubstFormat; 1662 1663 /* 1664 Lookup['Type'] Enumeration table for glyph substitution 1665 Value Type Description 1666 1 Single Replace one glyph with one glyph 1667 2 Multiple Replace one glyph with more than one glyph 1668 3 Alternate Replace one glyph with one of many glyphs 1669 4 Ligature Replace multiple glyphs with one glyph 1670 5 Context Replace one or more glyphs in context 1671 6 Chaining Context Replace one or more glyphs in chained context 1672 7 Extension Substitution Extension mechanism for other substitutions (i.e. this excludes the Extension type substitution itself) 1673 8 Reverse chaining context single Applied in reverse order, replace single glyph in chaining context 1674 */ 1675 1676 // LookupType 1: Single Substitution Subtable 1677 if ($Lookup[$i]['Type'] == 1) { 1678 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1679 if ($SubstFormat == 1) { // Calculated output glyph indices 1680 $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'] = $this->read_short(); 1681 } elseif ($SubstFormat == 2) { // Specified output glyph indices 1682 $GlyphCount = $this->read_ushort(); 1683 for ($g = 0; $g < $GlyphCount; $g++) { 1684 $Lookup[$i]['Subtable'][$c]['Glyphs'][] = $this->read_ushort(); 1685 } 1686 } 1687 } // LookupType 2: Multiple Substitution Subtable 1688 elseif ($Lookup[$i]['Type'] == 2) { 1689 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1690 $Lookup[$i]['Subtable'][$c]['SequenceCount'] = $SequenceCount = $this->read_short(); 1691 for ($s = 0; $s < $SequenceCount; $s++) { 1692 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1693 } 1694 for ($s = 0; $s < $SequenceCount; $s++) { 1695 // Sequence Tables 1696 $this->seek($Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset']); 1697 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount'] = $this->read_short(); 1698 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount']; $g++) { 1699 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['SubstituteGlyphID'][] = $this->read_ushort(); 1700 } 1701 } 1702 } // LookupType 3: Alternate Forms 1703 elseif ($Lookup[$i]['Type'] == 3) { 1704 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1705 $Lookup[$i]['Subtable'][$c]['AlternateSetCount'] = $AlternateSetCount = $this->read_short(); 1706 for ($s = 0; $s < $AlternateSetCount; $s++) { 1707 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1708 } 1709 1710 for ($s = 0; $s < $AlternateSetCount; $s++) { 1711 // AlternateSet Tables 1712 $this->seek($Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset']); 1713 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount'] = $this->read_short(); 1714 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount']; $g++) { 1715 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['SubstituteGlyphID'][] = $this->read_ushort(); 1716 } 1717 } 1718 } // LookupType 4: Ligature Substitution Subtable 1719 elseif ($Lookup[$i]['Type'] == 4) { 1720 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1721 $Lookup[$i]['Subtable'][$c]['LigSetCount'] = $LigSetCount = $this->read_short(); 1722 for ($s = 0; $s < $LigSetCount; $s++) { 1723 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1724 } 1725 for ($s = 0; $s < $LigSetCount; $s++) { 1726 // LigatureSet Tables 1727 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset']); 1728 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount'] = $this->read_short(); 1729 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) { 1730 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g] = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] + $this->read_ushort(); 1731 } 1732 } 1733 for ($s = 0; $s < $LigSetCount; $s++) { 1734 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) { 1735 // Ligature tables 1736 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g]); 1737 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph'] = $this->read_ushort(); 1738 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount'] = $this->read_ushort(); 1739 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) { 1740 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l] = $this->read_ushort(); 1741 } 1742 } 1743 } 1744 } // LookupType 5: Contextual Substitution Subtable 1745 elseif ($Lookup[$i]['Type'] == 5) { 1746 // Format 1: Context Substitution 1747 if ($SubstFormat == 1) { 1748 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1749 $Lookup[$i]['Subtable'][$c]['SubRuleSetCount'] = $SubRuleSetCount = $this->read_short(); 1750 for ($s = 0; $s < $SubRuleSetCount; $s++) { 1751 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1752 } 1753 for ($s = 0; $s < $SubRuleSetCount; $s++) { 1754 // SubRuleSet Tables 1755 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset']); 1756 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount'] = $this->read_short(); 1757 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) { 1758 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] + $this->read_ushort(); 1759 } 1760 } 1761 for ($s = 0; $s < $SubRuleSetCount; $s++) { 1762 // SubRule Tables 1763 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) { 1764 // Ligature tables 1765 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g]); 1766 1767 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount'] = $this->read_ushort(); 1768 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount'] = $this->read_ushort(); 1769 // "Input"::[GlyphCount - 1]::Array of input GlyphIDs-start with second glyph 1770 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount']; $l++) { 1771 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['Input'][$l] = $this->read_ushort(); 1772 } 1773 // "SubstLookupRecord"::[SubstCount]::Array of SubstLookupRecords-in design order 1774 for ($l = 0; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount']; $l++) { 1775 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['SequenceIndex'] = $this->read_ushort(); 1776 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['LookupListIndex'] = $this->read_ushort(); 1777 } 1778 } 1779 } 1780 } // Format 2: Class-based Context Glyph Substitution 1781 elseif ($SubstFormat == 2) { 1782 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1783 $Lookup[$i]['Subtable'][$c]['ClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1784 $Lookup[$i]['Subtable'][$c]['SubClassSetCnt'] = $this->read_ushort(); 1785 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $b++) { 1786 $offset = $this->read_ushort(); 1787 if ($offset == 0x0000) { 1788 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = 0; 1789 } else { 1790 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset; 1791 } 1792 } 1793 } else { 1794 throw new \Mpdf\Exception\FontException("GPOS Lookup Type " . $Lookup[$i]['Type'] . ", Format " . $SubstFormat . " not supported (ttfontsuni.php)."); 1795 } 1796 } // LookupType 6: Chaining Contextual Substitution Subtable 1797 elseif ($Lookup[$i]['Type'] == 6) { 1798 // Format 1: Simple Chaining Context Glyph Substitution p255 1799 if ($SubstFormat == 1) { 1800 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1801 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount'] = $this->read_ushort(); 1802 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $b++) { 1803 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1804 } 1805 } // Format 2: Class-based Chaining Context Glyph Substitution p257 1806 elseif ($SubstFormat == 2) { 1807 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1808 $Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1809 $Lookup[$i]['Subtable'][$c]['InputClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1810 $Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1811 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt'] = $this->read_ushort(); 1812 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $b++) { 1813 $offset = $this->read_ushort(); 1814 if ($offset == 0x0000) { 1815 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $offset; 1816 } else { 1817 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset; 1818 } 1819 } 1820 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259 1821 elseif ($SubstFormat == 3) { 1822 $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount'] = $this->read_ushort(); 1823 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) { 1824 $Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1825 } 1826 $Lookup[$i]['Subtable'][$c]['InputGlyphCount'] = $this->read_ushort(); 1827 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) { 1828 $Lookup[$i]['Subtable'][$c]['CoverageInput'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1829 } 1830 $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount'] = $this->read_ushort(); 1831 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) { 1832 $Lookup[$i]['Subtable'][$c]['CoverageLookahead'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1833 } 1834 $Lookup[$i]['Subtable'][$c]['SubstCount'] = $this->read_ushort(); 1835 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) { 1836 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'] = $this->read_ushort(); 1837 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'] = $this->read_ushort(); 1838 // Substitution Lookup Record 1839 // All contextual substitution subtables specify the substitution data in a Substitution Lookup Record 1840 // (SubstLookupRecord). Each record contains a SequenceIndex, which indicates the position where the substitution 1841 // will occur in the glyph sequence. In addition, a LookupListIndex identifies the lookup to be applied at the 1842 // glyph position specified by the SequenceIndex. 1843 } 1844 } 1845 } else { 1846 throw new \Mpdf\Exception\FontException(sprintf('Lookup Type "%s" not supported.', $Lookup[$i]['Type'])); 1847 } 1848 } 1849 } 1850 1851 // Process (2) Whole LookupList 1852 // Get Coverage tables and prepare preg_replace 1853 for ($i = 0; $i < $LookupCount; $i++) { 1854 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1855 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format']; 1856 1857 // LookupType 1: Single Substitution Subtable 1 => 1 1858 if ($Lookup[$i]['Type'] == 1) { 1859 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1860 $glyphs = $this->_getCoverage(false); 1861 for ($g = 0; $g < count($glyphs); $g++) { 1862 $replace = []; 1863 $substitute = []; 1864 $replace[] = unicode_hex($this->glyphToChar[$glyphs[$g]][0]); 1865 // Flag = Ignore 1866 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1867 continue; 1868 } 1869 if (isset($Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])) { // Format 1 1870 $substitute[] = unicode_hex($this->glyphToChar[($glyphs[$g] + $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])][0]); 1871 } else { // Format 2 1872 $substitute[] = unicode_hex($this->glyphToChar[($Lookup[$i]['Subtable'][$c]['Glyphs'][$g])][0]); 1873 } 1874 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute]; 1875 } 1876 } // LookupType 2: Multiple Substitution Subtable 1 => n 1877 elseif ($Lookup[$i]['Type'] == 2) { 1878 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1879 $glyphs = $this->_getCoverage(); 1880 for ($g = 0; $g < count($glyphs); $g++) { 1881 $replace = []; 1882 $substitute = []; 1883 $replace[] = $glyphs[$g]; 1884 // Flag = Ignore 1885 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1886 continue; 1887 } 1888 if (!isset($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) || count($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) == 0) { 1889 continue; 1890 } // Illegal for GlyphCount to be 0; either error in font, or something has gone wrong - lets carry on for now! 1891 foreach ($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID'] as $sub) { 1892 $substitute[] = unicode_hex($this->glyphToChar[$sub][0]); 1893 } 1894 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute]; 1895 } 1896 } // LookupType 3: Alternate Forms 1 => 1 (only first alternate form is used) 1897 elseif ($Lookup[$i]['Type'] == 3) { 1898 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1899 $glyphs = $this->_getCoverage(); 1900 for ($g = 0; $g < count($glyphs); $g++) { 1901 $replace = []; 1902 $substitute = []; 1903 $replace[] = $glyphs[$g]; 1904 // Flag = Ignore 1905 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1906 continue; 1907 } 1908 $gid = $Lookup[$i]['Subtable'][$c]['AlternateSets'][$g]['SubstituteGlyphID'][0]; 1909 if (!isset($this->glyphToChar[$gid][0])) { 1910 continue; 1911 } 1912 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]); 1913 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute]; 1914 } 1915 } // LookupType 4: Ligature Substitution Subtable n => 1 1916 elseif ($Lookup[$i]['Type'] == 4) { 1917 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1918 $glyphs = $this->_getCoverage(); 1919 $LigSetCount = $Lookup[$i]['Subtable'][$c]['LigSetCount']; 1920 for ($s = 0; $s < $LigSetCount; $s++) { 1921 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) { 1922 $replace = []; 1923 $substitute = []; 1924 $replace[] = $glyphs[$s]; 1925 // Flag = Ignore 1926 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1927 continue; 1928 } 1929 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) { 1930 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l]; 1931 $rpl = unicode_hex($this->glyphToChar[$gid][0]); 1932 // Flag = Ignore 1933 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $rpl, $Lookup[$i]['MarkFilteringSet'])) { 1934 continue 2; 1935 } 1936 $replace[] = $rpl; 1937 } 1938 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph']; 1939 if (!isset($this->glyphToChar[$gid][0])) { 1940 continue; 1941 } 1942 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]); 1943 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute, 'CompCount' => $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']]; 1944 } 1945 } 1946 } // LookupType 5: Contextual Substitution Subtable 1947 elseif ($Lookup[$i]['Type'] == 5) { 1948 // Format 1: Context Substitution 1949 if ($SubstFormat == 1) { 1950 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1951 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 1952 1953 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) { 1954 $SubRuleSet = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]; 1955 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph'] = $CoverageGlyphs[$s]; 1956 for ($r = 0; $r < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $r++) { 1957 $GlyphCount = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['GlyphCount']; 1958 for ($g = 1; $g < $GlyphCount; $g++) { 1959 $glyphID = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['Input'][$g]; 1960 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 1961 } 1962 } 1963 } 1964 } // Format 2: Class-based Context Glyph Substitution 1965 elseif ($SubstFormat == 2) { 1966 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1967 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 1968 1969 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['ClassDefOffset']); 1970 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses; 1971 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) { 1972 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) { 1973 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s]); 1974 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt'] = $SubClassRuleCnt = $this->read_ushort(); 1975 $SubClassRule = []; 1976 for ($b = 0; $b < $SubClassRuleCnt; $b++) { 1977 $SubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] + $this->read_ushort(); 1978 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $SubClassRule[$b]; 1979 } 1980 } 1981 } 1982 1983 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) { 1984 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) { 1985 $SubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt']; 1986 for ($b = 0; $b < $SubClassRuleCnt; $b++) { 1987 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b]); 1988 $Rule = []; 1989 $Rule['InputGlyphCount'] = $this->read_ushort(); 1990 $Rule['SubstCount'] = $this->read_ushort(); 1991 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) { 1992 $Rule['Input'][$r] = $this->read_ushort(); 1993 } 1994 for ($r = 0; $r < $Rule['SubstCount']; $r++) { 1995 $Rule['SequenceIndex'][$r] = $this->read_ushort(); 1996 $Rule['LookupListIndex'][$r] = $this->read_ushort(); 1997 } 1998 1999 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $Rule; 2000 } 2001 } 2002 } 2003 } // Format 3: Coverage-based Context Glyph Substitution 2004 elseif ($SubstFormat == 3) { 2005 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) { 2006 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]); 2007 $glyphs = $this->_getCoverage(); 2008 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs); 2009 } 2010 throw new \Mpdf\Exception\FontException("Lookup Type 5, SubstFormat 3 not tested. Please report this with the name of font used - " . $this->fontkey); 2011 } 2012 } // LookupType 6: Chaining Contextual Substitution Subtable 2013 elseif ($Lookup[$i]['Type'] == 6) { 2014 // Format 1: Simple Chaining Context Glyph Substitution p255 2015 if ($SubstFormat == 1) { 2016 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 2017 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 2018 2019 $ChainSubRuleSetCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; 2020 2021 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) { 2022 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s]); 2023 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount'] = $this->read_ushort(); 2024 for ($r = 0; $r < $ChainSubRuleCnt; $r++) { 2025 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r] = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s] + $this->read_ushort(); 2026 } 2027 } 2028 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) { 2029 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount']; 2030 for ($r = 0; $r < $ChainSubRuleCnt; $r++) { 2031 // ChainSubRule 2032 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r]); 2033 2034 $BacktrackGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphCount'] = $this->read_ushort(); 2035 for ($g = 0; $g < $BacktrackGlyphCount; $g++) { 2036 $glyphID = $this->read_ushort(); 2037 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 2038 } 2039 2040 $InputGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphCount'] = $this->read_ushort(); 2041 for ($g = 1; $g < $InputGlyphCount; $g++) { 2042 $glyphID = $this->read_ushort(); 2043 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 2044 } 2045 2046 $LookaheadGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphCount'] = $this->read_ushort(); 2047 for ($g = 0; $g < $LookaheadGlyphCount; $g++) { 2048 $glyphID = $this->read_ushort(); 2049 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 2050 } 2051 2052 $SubstCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SubstCount'] = $this->read_ushort(); 2053 for ($lu = 0; $lu < $SubstCount; $lu++) { 2054 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SequenceIndex'][$lu] = $this->read_ushort(); 2055 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookupListIndex'][$lu] = $this->read_ushort(); 2056 } 2057 } 2058 } 2059 } // Format 2: Class-based Chaining Context Glyph Substitution p257 2060 elseif ($SubstFormat == 2) { 2061 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 2062 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 2063 2064 $BacktrackClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset']); 2065 $Lookup[$i]['Subtable'][$c]['BacktrackClasses'] = $BacktrackClasses; 2066 2067 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['InputClassDefOffset']); 2068 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses; 2069 2070 $LookaheadClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset']); 2071 $Lookup[$i]['Subtable'][$c]['LookaheadClasses'] = $LookaheadClasses; 2072 2073 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) { 2074 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) { 2075 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s]); 2076 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'] = $ChainSubClassRuleCnt = $this->read_ushort(); 2077 $ChainSubClassRule = []; 2078 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) { 2079 $ChainSubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] + $this->read_ushort(); 2080 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $ChainSubClassRule[$b]; 2081 } 2082 } 2083 } 2084 2085 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) { 2086 if (isset($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'])) { 2087 $ChainSubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt']; 2088 } else { 2089 $ChainSubClassRuleCnt = 0; 2090 } 2091 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) { 2092 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) { 2093 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b]); 2094 $Rule = []; 2095 $Rule['BacktrackGlyphCount'] = $this->read_ushort(); 2096 for ($r = 0; $r < $Rule['BacktrackGlyphCount']; $r++) { 2097 $Rule['Backtrack'][$r] = $this->read_ushort(); 2098 } 2099 $Rule['InputGlyphCount'] = $this->read_ushort(); 2100 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) { 2101 $Rule['Input'][$r] = $this->read_ushort(); 2102 } 2103 $Rule['LookaheadGlyphCount'] = $this->read_ushort(); 2104 for ($r = 0; $r < $Rule['LookaheadGlyphCount']; $r++) { 2105 $Rule['Lookahead'][$r] = $this->read_ushort(); 2106 } 2107 $Rule['SubstCount'] = $this->read_ushort(); 2108 for ($r = 0; $r < $Rule['SubstCount']; $r++) { 2109 $Rule['SequenceIndex'][$r] = $this->read_ushort(); 2110 $Rule['LookupListIndex'][$r] = $this->read_ushort(); 2111 } 2112 2113 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $Rule; 2114 } 2115 } 2116 } 2117 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259 2118 elseif ($SubstFormat == 3) { 2119 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) { 2120 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][$b]); 2121 $glyphs = $this->_getCoverage(); 2122 $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'][] = implode("|", $glyphs); 2123 } 2124 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) { 2125 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]); 2126 $glyphs = $this->_getCoverage(); 2127 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs); 2128 // Don't use above value as these are ordered numerically not as need to process 2129 } 2130 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) { 2131 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageLookahead'][$b]); 2132 $glyphs = $this->_getCoverage(); 2133 $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'][] = implode("|", $glyphs); 2134 } 2135 } 2136 } 2137 } 2138 } 2139 2140 $GSUBScriptLang = []; 2141 $rtlpua = []; // All glyphs added to PUA [for magic_reverse] 2142 foreach ($gsub as $st => $scripts) { 2143 foreach ($scripts as $t => $langsys) { 2144 $lul = []; // array of LookupListIndexes 2145 $tags = []; // corresponding array of feature tags e.g. 'ccmp' 2146 2147 foreach ($langsys as $tag => $ft) { 2148 foreach ($ft as $ll) { 2149 $lul[$ll] = $tag; 2150 } 2151 } 2152 ksort($lul); // Order the Lookups in the order they are in the GUSB table, regardless of Feature order 2153 $volt = $this->_getGSUBarray($Lookup, $lul, $st); 2154 2155 // Interrogate $volt 2156 // isol, fin, medi, init(arab syrc) into $rtlSUB for use in ArabJoin 2157 // but also identify all RTL chars in PUA for magic_reverse (arab syrc hebr thaa nko samr) 2158 // identify reph, matras, vatu, half forms etc for Indic for final re-ordering 2159 $rtl = []; 2160 $rtlSUB = []; 2161 $finals = ''; 2162 2163 if (strpos('arab syrc hebr thaa nko samr', $st) !== false) { // all RTL scripts [any/all languages] ? Mandaic 2164 2165 foreach ($volt as $v) { 2166 // isol fina fin2 fin3 medi med2 for Syriac 2167 // ISOLATED FORM :: FINAL :: INITIAL :: MEDIAL :: MED2 :: FIN2 :: FIN3 2168 if (strpos('isol fina init medi fin2 fin3 med2', $v['tag']) !== false) { 2169 2170 $key = $v['match']; 2171 $key = preg_replace('/[\(\)]*/', '', $key); 2172 $sub = $v['replace']; 2173 if ($v['tag'] === 'isol') { 2174 $kk = 0; 2175 } elseif ($v['tag'] === 'fina') { 2176 $kk = 1; 2177 } elseif ($v['tag'] === 'init') { 2178 $kk = 2; 2179 } elseif ($v['tag'] === 'medi') { 2180 $kk = 3; 2181 } elseif ($v['tag'] === 'med2') { 2182 $kk = 4; 2183 } elseif ($v['tag'] === 'fin2') { 2184 $kk = 5; 2185 } elseif ($v['tag'] === 'fin3') { 2186 $kk = 6; 2187 } 2188 2189 $rtl[$key][$kk] = $sub; 2190 if (isset($v['prel']) && count($v['prel'])) { 2191 $rtl[$key]['prel'][$kk] = $v['prel']; 2192 } 2193 if (isset($v['postl']) && count($v['postl'])) { 2194 $rtl[$key]['postl'][$kk] = $v['postl']; 2195 } 2196 if (isset($v['ignore']) && $v['ignore']) { 2197 $rtl[$key]['ignore'][$kk] = $v['ignore']; 2198 } 2199 $rtlpua[] = $sub; 2200 2201 } else { // Add any other glyphs which are in PUA 2202 if (isset($v['context']) && $v['context']) { 2203 foreach ($v['rules'] as $vs) { 2204 $matchCount = count($vs['match']); 2205 for ($i = 0; $i < $matchCount; $i++) { 2206 if (isset($vs['replace'][$i]) && preg_match('/^0[A-F0-9]{4}$/', $vs['match'][$i])) { 2207 if (preg_match('/^0[EF][A-F0-9]{3}$/', $vs['replace'][$i])) { 2208 $rtlpua[] = $vs['replace'][$i]; 2209 } 2210 } 2211 } 2212 } 2213 } else { 2214 preg_match_all('/\((0[A-F0-9]{4})\)/', $v['match'], $m); 2215 $matchCount = count($m[0]); 2216 for ($i = 0; $i < $matchCount; $i++) { 2217 $sb = explode(' ', $v['replace']); 2218 foreach ($sb as $sbg) { 2219 if (preg_match('/(0[EF][A-F0-9]{3})/', $sbg, $mr)) { 2220 $rtlpua[] = $mr[1]; 2221 } 2222 } 2223 } 2224 } 2225 } 2226 } 2227 2228 // For kashida, need to determine all final forms except ones already identified by kashida priority rules (see \Mpdf\Otl) 2229 foreach ($rtl as $base => $variants) { 2230 if (isset($variants[1])) { // i.e. final form 2231 if (strpos('0FE8E 0FE94 0FEA2 0FEAA 0FEAE 0FEC2 0FEDA 0FEDE 0FB93 0FECA 0FED2 0FED6 0FEEE 0FEF0 0FEF2', $variants[1]) === false) { // not already included 2232 // This version does not exclude RA (0631) FEAE; Ya (064A) FEF2; Alef Maqsurah (0649) FEF0 which 2233 // are selected in priority if connected to a medial Bah 2234 //if (strpos('0FE8E 0FE94 0FEA2 0FEAA 0FEC2 0FEDA 0FEDE 0FB93 0FECA 0FED2 0FED6 0FEEE', $variants[1])===false) { // not already included 2235 $finals .= $variants[1] . ' '; 2236 } 2237 } 2238 } 2239 2240 ksort($rtl); 2241 $rtlSUB = $rtl; 2242 } 2243 2244 // INDIC - Dynamic properties 2245 $rphf = []; 2246 $half = []; 2247 $pref = []; 2248 $blwf = []; 2249 $pstf = []; 2250 2251 if (strpos('dev2 bng2 gur2 gjr2 ory2 tml2 tel2 knd2 mlm2 deva beng guru gujr orya taml telu knda mlym', $st) !== false) { // all INDIC scripts [any/all languages] 2252 if (strpos('deva beng guru gujr orya taml telu knda mlym', $st) !== false) { 2253 $is_old_spec = true; 2254 } else { 2255 $is_old_spec = false; 2256 } 2257 2258 // First get 'locl' substitutions (reversed!) 2259 $loclsubs = []; 2260 foreach ($volt as $v) { 2261 if (strpos('locl', $v['tag']) !== false) { 2262 $key = $v['match']; 2263 $key = preg_replace('/[\(\)]*/', '', $key); 2264 $sub = $v['replace']; 2265 if ($key && strlen(trim($key)) == 5 && $sub) { 2266 $loclsubs[$sub] = $key; 2267 } 2268 } 2269 } 2270 2271 foreach ($volt as $v) { 2272 // <rphf> <half> <pref> <blwf> <pstf> 2273 // defines consonant types: 2274 // Reph <rphf> 2275 // Half forms <half> 2276 // Pre-base-reordering forms of Ra/Rra <pref> 2277 // Below-base forms <blwf> 2278 // Post-base forms <pstf> 2279 // applied together with <locl> feature to input sequences consisting of two characters 2280 // This is done for each consonant 2281 // for <rphf> and <half>, features are applied to Consonant + Halant combinations 2282 // for <pref>, <blwf> and <pstf>, features are applied to Halant + Consonant combinations 2283 // Old version eg 'deva' <pref>, <blwf> and <pstf>, features are applied to Consonant + Halant 2284 // Some malformed fonts still do Consonant + Halant for these - so match both?? 2285 // If these two glyphs form a ligature, with no additional glyphs in context 2286 // this means the consonant has the corresponding form 2287 // Currently set to cope with both 2288 // See also classes/otl.php 2289 2290 if (strpos('rphf half pref blwf pstf', $v['tag']) !== false) { 2291 if (isset($v['context']) && $v['context'] && $v['nBacktrack'] == 0 && $v['nLookahead'] == 0) { 2292 foreach ($v['rules'] as $vs) { 2293 if (count($vs['match']) == 2 && count($vs['replace']) == 1) { 2294 $sub = $vs['replace'][0]; 2295 // If Halant Cons <pref>, <blwf> and <pstf> in New version only 2296 if (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', $vs['match'][0]) !== false && strpos('pref blwf pstf', $v['tag']) !== false && !$is_old_spec) { 2297 $key = $vs['match'][1]; 2298 $tag = $v['tag']; 2299 if (isset($loclsubs[$key])) { 2300 ${$tag[$loclsubs[$key]]} = $sub; 2301 } 2302 $tmp = &$$tag; 2303 $tmp[hexdec($key)] = hexdec($sub); 2304 } // If Cons Halant <rphf> and <half> always 2305 // and <pref>, <blwf> and <pstf> in Old version 2306 elseif (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', $vs['match'][1]) !== false && (strpos('rphf half', $v['tag']) !== false || (strpos('pref blwf pstf', $v['tag']) !== false && ($is_old_spec || _OTL_OLD_SPEC_COMPAT_2)))) { 2307 $key = $vs['match'][0]; 2308 $tag = $v['tag']; 2309 if (isset($loclsubs[$key])) { 2310 ${$tag[$loclsubs[$key]]} = $sub; 2311 } 2312 $tmp = &$$tag; 2313 $tmp[hexdec($key)] = hexdec($sub); 2314 } 2315 } 2316 } 2317 } elseif (!isset($v['context'])) { 2318 $key = $v['match']; 2319 $key = preg_replace('/[\(\)]*/', '', $key); 2320 $sub = $v['replace']; 2321 if ($key && strlen(trim($key)) == 11 && $sub) { 2322 // If Cons Halant <rphf> and <half> always 2323 // and <pref>, <blwf> and <pstf> in Old version 2324 // If Halant Cons <pref>, <blwf> and <pstf> in New version only 2325 if (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', substr($key, 0, 5)) !== false && strpos('pref blwf pstf', $v['tag']) !== false && !$is_old_spec) { 2326 $key = substr($key, 6, 5); 2327 $tag = $v['tag']; 2328 if (isset($loclsubs[$key])) { 2329 ${$tag[$loclsubs[$key]]} = $sub; 2330 } 2331 $tmp = &$$tag; 2332 $tmp[hexdec($key)] = hexdec($sub); 2333 } elseif (strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', substr($key, 6, 5)) !== false && (strpos('rphf half', $v['tag']) !== false || (strpos('pref blwf pstf', $v['tag']) !== false && ($is_old_spec || _OTL_OLD_SPEC_COMPAT_2)))) { 2334 $key = substr($key, 0, 5); 2335 $tag = $v['tag']; 2336 if (isset($loclsubs[$key])) { 2337 ${$tag[$loclsubs[$key]]} = $sub; 2338 } 2339 $tmp = &$$tag; 2340 $tmp[hexdec($key)] = hexdec($sub); 2341 } 2342 } 2343 } 2344 } 2345 } 2346 } 2347 2348 if (count($rtl) || count($rphf) || count($half) || count($pref) || count($blwf) || count($pstf) || $finals) { 2349 $font = [ 2350 'rtlSUB' => $rtlSUB, 2351 'finals' => $finals, 2352 'rphf' => $rphf, 2353 'half' => $half, 2354 'pref' => $pref, 2355 'blwf' => $blwf, 2356 'pstf' => $pstf, 2357 ]; 2358 2359 $this->fontCache->jsonWrite($this->fontkey . '.GSUB.' . $st . '.' . $t . '.json', $font); 2360 } 2361 2362 if (!isset($GSUBScriptLang[$st])) { 2363 $GSUBScriptLang[$st] = ''; 2364 } 2365 $GSUBScriptLang[$st] .= $t . ' '; 2366 } 2367 } 2368 2369 // All RTL glyphs from font added to (or already in) PUA [reqd for magic_reverse] 2370 $rtlPUAstr = ''; 2371 if (count($rtlpua)) { 2372 $rtlpua = array_unique($rtlpua); 2373 sort($rtlpua); 2374 $n = count($rtlpua); 2375 for ($i = 0; $i < $n; $i++) { 2376 if (hexdec($rtlpua[$i]) < hexdec('E000') || hexdec($rtlpua[$i]) > hexdec('F8FF')) { 2377 unset($rtlpua[$i]); 2378 } 2379 } 2380 sort($rtlpua, SORT_STRING); 2381 2382 $rangeid = -1; 2383 $range = []; 2384 $prevgid = -2; 2385 2386 // for each character 2387 foreach ($rtlpua as $gidhex) { 2388 $gid = hexdec($gidhex); 2389 if ($gid == ($prevgid + 1)) { 2390 $range[$rangeid]['end'] = $gidhex; 2391 $range[$rangeid]['count']++; 2392 } else { 2393 // new range 2394 $rangeid++; 2395 $range[$rangeid] = []; 2396 $range[$rangeid]['start'] = $gidhex; 2397 $range[$rangeid]['end'] = $gidhex; 2398 $range[$rangeid]['count'] = 1; 2399 } 2400 $prevgid = $gid; 2401 } 2402 2403 foreach ($range as $rg) { 2404 if ($rg['count'] == 1) { 2405 $rtlPUAstr .= "\x{" . $rg['start'] . "}"; 2406 } elseif ($rg['count'] == 2) { 2407 $rtlPUAstr .= "\x{" . $rg['start'] . "}\x{" . $rg['end'] . "}"; 2408 } else { 2409 $rtlPUAstr .= "\x{" . $rg['start'] . "}-\x{" . $rg['end'] . "}"; 2410 } 2411 } 2412 } 2413 2414 return [$GSUBScriptLang, $gsub, $GSLookup, $rtlPUAstr]; 2415 } 2416 2417 // GSUB functions 2418 function _getGSUBarray(&$Lookup, &$lul, $scripttag) 2419 { 2420 // Process (3) LookupList for specific Script-LangSys 2421 // Generate preg_replace 2422 $volt = []; 2423 $reph = ''; 2424 $matraE = ''; 2425 $vatu = ''; 2426 2427 foreach ($lul as $i => $tag) { 2428 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 2429 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format']; 2430 2431 // LookupType 1: Single Substitution Subtable 2432 if ($Lookup[$i]['Type'] == 1) { 2433 $subCount = count($Lookup[$i]['Subtable'][$c]['subs']); 2434 for ($s = 0; $s < $subCount; $s++) { 2435 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2436 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; 2437 // Ignore has already been applied earlier on 2438 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()"); 2439 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0); 2440 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 1]; 2441 } 2442 } // LookupType 2: Multiple Substitution Subtable 2443 elseif ($Lookup[$i]['Type'] == 2) { 2444 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { 2445 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2446 $substitute = implode(" ", $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute']); 2447 // Ignore has already been applied earlier on 2448 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()"); 2449 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0); 2450 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 2]; 2451 } 2452 } // LookupType 3: Alternate Forms 2453 elseif ($Lookup[$i]['Type'] == 3) { 2454 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { 2455 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2456 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; 2457 // Ignore has already been applied earlier on 2458 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()"); 2459 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0); 2460 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 3]; 2461 } 2462 } // LookupType 4: Ligature Substitution Subtable 2463 elseif ($Lookup[$i]['Type'] == 4) { 2464 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { 2465 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2466 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; 2467 // Ignore has already been applied earlier on 2468 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2469 $repl = $this->_makeGSUBinputMatch($inputGlyphs, $ignore); 2470 $subs = $this->_makeGSUBinputReplacement(count($inputGlyphs), $substitute, $ignore, 0, count($inputGlyphs), 0); 2471 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 4, 'CompCount' => $Lookup[$i]['Subtable'][$c]['subs'][$s]['CompCount'], 'Lig' => $substitute]; 2472 } 2473 } // LookupType 5: Chaining Contextual Substitution Subtable 2474 elseif ($Lookup[$i]['Type'] == 5) { 2475 // Format 1: Context Substitution 2476 if ($SubstFormat == 1) { 2477 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2478 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) { 2479 // SubRuleSet 2480 $subRule = []; 2481 foreach ($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'] as $rule) { 2482 // SubRule 2483 $inputGlyphs = []; 2484 if ($rule['GlyphCount'] > 1) { 2485 $inputGlyphs = $rule['InputGlyphs']; 2486 } 2487 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph']; 2488 ksort($inputGlyphs); 2489 $nInput = count($inputGlyphs); 2490 2491 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2492 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => '', 'match' => $contextInputMatch, 'nBacktrack' => 0, 'nInput' => $nInput, 'nLookahead' => 0, 'rules' => [],]; 2493 2494 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2495 $lup = $rule['SubstLookupRecord'][$b]['LookupListIndex']; 2496 $seqIndex = $rule['SubstLookupRecord'][$b]['SequenceIndex']; 2497 2498 // $Lookup[$lup] = secondary Lookup 2499 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2500 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2501 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2502 $lookupGlyphs = $luss['Replace']; 2503 $mLen = count($lookupGlyphs); 2504 2505 // Only apply if the (first) 'Replace' glyph from the 2506 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2507 // then apply the substitution 2508 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2509 continue; 2510 } 2511 $REPL = implode(" ", $luss['substitute']); 2512 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2513 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2514 } else { 2515 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2516 } 2517 } 2518 } 2519 } 2520 } 2521 2522 if (count($subRule['rules'])) { 2523 $volt[] = $subRule; 2524 } 2525 } 2526 } 2527 } // Format 2: Class-based Context Glyph Substitution 2528 elseif ($SubstFormat == 2) { 2529 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2530 foreach ($Lookup[$i]['Subtable'][$c]['SubClassSet'] as $inputClass => $cscs) { 2531 for ($cscrule = 0; $cscrule < $cscs['SubClassRuleCnt']; $cscrule++) { 2532 $rule = $cscs['SubClassRule'][$cscrule]; 2533 2534 $inputGlyphs = []; 2535 2536 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass]; 2537 if ($rule['InputGlyphCount'] > 1) { 2538 // NB starts at 1 2539 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) { 2540 $classindex = $rule['Input'][$gcl]; 2541 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex])) { 2542 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex]; 2543 } // if class[0] = all glyphs excluding those specified in all other classes 2544 // set to blank '' for now 2545 else { 2546 $inputGlyphs[$gcl] = ''; 2547 } 2548 } 2549 } 2550 2551 $nInput = $rule['InputGlyphCount']; 2552 $nIsubs = (2 * $nInput) - 1; 2553 2554 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2555 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => '', 'match' => $contextInputMatch, 'nBacktrack' => 0, 'nInput' => $nInput, 'nLookahead' => 0, 'rules' => [],]; 2556 2557 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2558 $lup = $rule['LookupListIndex'][$b]; 2559 $seqIndex = $rule['SequenceIndex'][$b]; 2560 2561 // $Lookup[$lup] = secondary Lookup 2562 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2563 if (isset($Lookup[$lup]['Subtable'][$lus]['subs']) && count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2564 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2565 $lookupGlyphs = $luss['Replace']; 2566 $mLen = count($lookupGlyphs); 2567 2568 // Only apply if the (first) 'Replace' glyph from the 2569 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2570 // then apply the substitution 2571 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2572 continue; 2573 } 2574 2575 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2576 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2577 $REPL = implode(" ", $luss['substitute']); 2578 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}" 2579 2580 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2581 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2582 } else { 2583 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2584 } 2585 } 2586 } 2587 } 2588 } 2589 if (count($subRule['rules'])) { 2590 $volt[] = $subRule; 2591 } 2592 } 2593 } 2594 2595 } // Format 3: Coverage-based Context Glyph Substitution p259 2596 elseif ($SubstFormat == 3) { 2597 2598 // IgnoreMarks flag set on main Lookup table 2599 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2600 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs']; 2601 $CoverageInputGlyphs = implode('|', $inputGlyphs); 2602 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; 2603 2604 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) { 2605 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs']; 2606 } else { 2607 $backtrackGlyphs = []; 2608 } 2609 2610 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 2611 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2612 2613 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) { 2614 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs']; 2615 } else { 2616 $lookaheadGlyphs = []; 2617 } 2618 2619 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 2620 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2621 2622 $nBsubs = 2 * count($backtrackGlyphs); 2623 $nIsubs = (2 * $nInput) - 1; 2624 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2625 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2626 2627 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) { 2628 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex']; 2629 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex']; 2630 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2631 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2632 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2633 $lookupGlyphs = $luss['Replace']; 2634 $mLen = count($lookupGlyphs); 2635 2636 // Only apply if the (first) 'Replace' glyph from the 2637 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2638 // then apply the substitution 2639 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2640 continue; 2641 } 2642 2643 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2644 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2645 $REPL = implode(" ", $luss['substitute']); 2646 2647 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2648 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2649 } else { 2650 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2651 } 2652 } 2653 } 2654 } 2655 } 2656 if (count($subRule['rules'])) { 2657 $volt[] = $subRule; 2658 } 2659 } 2660 2661 } // LookupType 6: ing Contextual Substitution Subtable 2662 elseif ($Lookup[$i]['Type'] == 6) { 2663 2664 // Format 1: Simple Chaining Context Glyph Substitution p255 2665 if ($SubstFormat == 1) { 2666 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2667 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $s++) { 2668 2669 // ChainSubRuleSet 2670 $subRule = []; 2671 $firstInputGlyph = $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'][$s]; // First input gyyph 2672 2673 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'] as $rule) { 2674 // ChainSubRule 2675 $inputGlyphs = []; 2676 if ($rule['InputGlyphCount'] > 1) { 2677 $inputGlyphs = $rule['InputGlyphs']; 2678 } 2679 $inputGlyphs[0] = $firstInputGlyph; 2680 ksort($inputGlyphs); 2681 $nInput = count($inputGlyphs); 2682 2683 if ($rule['BacktrackGlyphCount']) { 2684 $backtrackGlyphs = $rule['BacktrackGlyphs']; 2685 } else { 2686 $backtrackGlyphs = []; 2687 } 2688 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2689 2690 if ($rule['LookaheadGlyphCount']) { 2691 $lookaheadGlyphs = $rule['LookaheadGlyphs']; 2692 } else { 2693 $lookaheadGlyphs = []; 2694 } 2695 2696 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2697 2698 $nBsubs = 2 * count($backtrackGlyphs); 2699 $nIsubs = (2 * $nInput) - 1; 2700 2701 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2702 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2703 2704 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2705 $lup = $rule['LookupListIndex'][$b]; 2706 $seqIndex = $rule['SequenceIndex'][$b]; 2707 2708 // $Lookup[$lup] = secondary Lookup 2709 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2710 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2711 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2712 $lookupGlyphs = $luss['Replace']; 2713 $mLen = count($lookupGlyphs); 2714 2715 // Only apply if the (first) 'Replace' glyph from the 2716 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2717 // then apply the substitution 2718 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2719 continue; 2720 } 2721 2722 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2723 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2724 2725 $REPL = implode(" ", $luss['substitute']); 2726 2727 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2728 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2729 } else { 2730 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2731 } 2732 } 2733 } 2734 } 2735 } 2736 2737 if (count($subRule['rules'])) { 2738 $volt[] = $subRule; 2739 } 2740 } 2741 } 2742 2743 } // Format 2: Class-based Chaining Context Glyph Substitution p257 2744 elseif ($SubstFormat == 2) { 2745 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2746 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'] as $inputClass => $cscs) { 2747 for ($cscrule = 0; $cscrule < $cscs['ChainSubClassRuleCnt']; $cscrule++) { 2748 $rule = $cscs['ChainSubClassRule'][$cscrule]; 2749 2750 // These contain classes of glyphs as strings 2751 // $Lookup[$i]['Subtable'][$c]['InputClasses'][(class)] e.g. 02E6|02E7|02E8 2752 // $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][(class)] 2753 // $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][(class)] 2754 // These contain arrays of classIndexes 2755 // [Backtrack] [Lookahead] and [Input] (Input is from the second position only) 2756 2757 $inputGlyphs = []; 2758 2759 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass])) { 2760 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass]; 2761 } else { 2762 $inputGlyphs[0] = ''; 2763 } 2764 if ($rule['InputGlyphCount'] > 1) { 2765 // NB starts at 1 2766 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) { 2767 $classindex = $rule['Input'][$gcl]; 2768 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex])) { 2769 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex]; 2770 } // if class[0] = all glyphs excluding those specified in all other classes 2771 // set to blank '' for now 2772 else { 2773 $inputGlyphs[$gcl] = ''; 2774 } 2775 } 2776 } 2777 2778 $nInput = $rule['InputGlyphCount']; 2779 2780 if ($rule['BacktrackGlyphCount']) { 2781 for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) { 2782 $classindex = $rule['Backtrack'][$gcl]; 2783 if (isset($Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex])) { 2784 $backtrackGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex]; 2785 } // if class[0] = all glyphs excluding those specified in all other classes 2786 // set to blank '' for now 2787 else { 2788 $backtrackGlyphs[$gcl] = ''; 2789 } 2790 } 2791 } else { 2792 $backtrackGlyphs = []; 2793 } 2794 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 2795 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2796 2797 if ($rule['LookaheadGlyphCount']) { 2798 for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) { 2799 $classindex = $rule['Lookahead'][$gcl]; 2800 if (isset($Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex])) { 2801 $lookaheadGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex]; 2802 } // if class[0] = all glyphs excluding those specified in all other classes 2803 // set to blank '' for now 2804 else { 2805 $lookaheadGlyphs[$gcl] = ''; 2806 } 2807 } 2808 } else { 2809 $lookaheadGlyphs = []; 2810 } 2811 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 2812 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2813 2814 $nBsubs = 2 * count($backtrackGlyphs); 2815 $nIsubs = (2 * $nInput) - 1; 2816 2817 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2818 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2819 2820 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2821 $lup = $rule['LookupListIndex'][$b]; 2822 $seqIndex = $rule['SequenceIndex'][$b]; 2823 2824 // $Lookup[$lup] = secondary Lookup 2825 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2826 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2827 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2828 $lookupGlyphs = $luss['Replace']; 2829 $mLen = count($lookupGlyphs); 2830 2831 // Only apply if the (first) 'Replace' glyph from the 2832 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2833 // then apply the substitution 2834 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2835 continue; 2836 } 2837 2838 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2839 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2840 $REPL = implode(" ", $luss['substitute']); 2841 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}" 2842 2843 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2844 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2845 } else { 2846 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2847 } 2848 } 2849 } 2850 } 2851 } 2852 if (count($subRule['rules'])) { 2853 $volt[] = $subRule; 2854 } 2855 } 2856 } 2857 2858 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259 2859 elseif ($SubstFormat == 3) { 2860 // IgnoreMarks flag set on main Lookup table 2861 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2862 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs']; 2863 $CoverageInputGlyphs = implode('|', $inputGlyphs); 2864 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; 2865 2866 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) { 2867 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs']; 2868 } else { 2869 $backtrackGlyphs = []; 2870 } 2871 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 2872 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2873 2874 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) { 2875 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs']; 2876 } else { 2877 $lookaheadGlyphs = []; 2878 } 2879 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 2880 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2881 2882 $nBsubs = 2 * count($backtrackGlyphs); 2883 $nIsubs = (2 * $nInput) - 1; 2884 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2885 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2886 2887 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) { 2888 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex']; 2889 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex']; 2890 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2891 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2892 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2893 $lookupGlyphs = $luss['Replace']; 2894 $mLen = count($lookupGlyphs); 2895 2896 // Only apply if the (first) 'Replace' glyph from the 2897 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2898 // then apply the substitution 2899 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2900 continue; 2901 } 2902 2903 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2904 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2905 $REPL = implode(" ", $luss['substitute']); 2906 2907 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2908 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2909 } else { 2910 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2911 } 2912 } 2913 } 2914 } 2915 } 2916 if (count($subRule['rules'])) { 2917 $volt[] = $subRule; 2918 } 2919 } 2920 } 2921 } 2922 } 2923 2924 return $volt; 2925 } 2926 2927 function _checkGSUBignore($flag, $glyph, $MarkFilteringSet) 2928 { 2929 $ignore = false; 2930 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType) 2931 if ((($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) && strpos($this->GlyphClassMarks, $glyph)) { 2932 $ignore = true; 2933 } 2934 if ((($flag & 0x0004) == 0x0004) && strpos($this->GlyphClassLigatures, $glyph)) { 2935 $ignore = true; 2936 } 2937 if ((($flag & 0x0002) == 0x0002) && strpos($this->GlyphClassBases, $glyph)) { 2938 $ignore = true; 2939 } 2940 // Flag & 0xFF?? = MarkAttachmentType 2941 if ($flag & 0xFF00) { 2942 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class" 2943 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table 2944 if (strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)) { 2945 $ignore = true; 2946 } 2947 } 2948 // Flag & 0x0010 = UseMarkFilteringSet 2949 if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) { 2950 $ignore = true; 2951 } 2952 2953 return $ignore; 2954 } 2955 2956 function _getGSUBignoreString($flag, $MarkFilteringSet) 2957 { 2958 // If ignoreFlag set, combine all ignore glyphs into -> "((?:(?: FBA1| FBA2| FBA3))*)" 2959 // else "()" 2960 // for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup 2961 $str = ""; 2962 $ignoreflag = 0; 2963 2964 // Flag & 0xFF?? = MarkAttachmentType 2965 if ($flag & 0xFF00) { 2966 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class" 2967 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table 2968 $MarkAttachmentType = $flag >> 8; 2969 $ignoreflag = $flag; 2970 $str = $this->MarkAttachmentType[$MarkAttachmentType]; 2971 } 2972 2973 // Flag & 0x0010 = UseMarkFilteringSet 2974 if ($flag & 0x0010) { 2975 throw new \Mpdf\Exception\FontException("This font " . $this->fontkey . " contains MarkGlyphSets - Not tested yet"); 2976 $str = $this->MarkGlyphSets[$MarkFilteringSet]; 2977 } 2978 2979 // If Ignore Marks set, supercedes any above 2980 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType) 2981 if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) { 2982 $ignoreflag = 8; 2983 $str = $this->GlyphClassMarks; 2984 } 2985 2986 // Flag & 0x0004 = Ignore Ligatures 2987 if (($flag & 0x0004) == 0x0004) { 2988 $ignoreflag += 4; 2989 if ($str) { 2990 $str .= "|"; 2991 } 2992 $str .= $this->GlyphClassLigatures; 2993 } 2994 // Flag & 0x0002 = Ignore BaseGlyphs 2995 if (($flag & 0x0002) == 0x0002) { 2996 $ignoreflag += 2; 2997 if ($str) { 2998 $str .= "|"; 2999 } 3000 $str .= $this->GlyphClassBases; 3001 } 3002 if ($str) { 3003 // This originally returned e.g. ((?:(?:[IGNORE8]))*) when NOT specific to a Lookup e.g. rtlSub in 3004 // arabictypesetting.GSUB.arab.DFLT.php 3005 // This would save repeatedly saving long text strings if used multiple times 3006 // When writing e.g. arabictypesetting.GSUB.arab.DFLT.php to file, included as $ignore[8] 3007 // Would need to also write the $ignore array to that file 3008 // // If UseMarkFilteringSet (specific to the Lookup) return the string 3009 // if (($flag & 0x0010) && ($flag & 0x0008) != 0x0008) { 3010 // return "((?:(?:" . $str . "))*)"; 3011 // } 3012 // else { return "((?:(?:" . "[IGNORE".$ignoreflag."]" . "))*)"; } 3013 // // e.g. ((?:(?: 0031| 0032| 0033| 0034| 0045))*) 3014 // But never finished coding it to add the $ignore array to the file, and it doesn't seem to occur often enough to be worth 3015 // writing. So just output it as a string: 3016 return "((?:(?:" . $str . "))*)"; 3017 } else { 3018 return "()"; 3019 } 3020 } 3021 3022 // GSUB Patterns 3023 3024 /* 3025 BACKTRACK INPUT LOOKAHEAD 3026 ================================== ================== ================================== 3027 (FEEB|FEEC)(ign) ¦(FD12|FD13)(ign) ¦(0612)¦(ign) (0613)¦(ign) (FD12|FD13)¦(ign) (FEEB|FEEC) 3028 ---------------- ---------------- ----- ------------ --------------- --------------- 3029 Backtrack 1 Backtrack 2 Input 1 Input 2 Lookahead 1 Lookahead 2 3030 -------- --- --------- --- ---- --- ---- --- --------- --- ------- 3031 \${1} \${2} \${3} \${4} \${5+} \${6+} \${7+} \${8+} 3032 3033 nBacktrack = 2 nInput = 2 nLookahead = 2 3034 3035 nBsubs = 2xnBack nIsubs = (nBsubs+) nLsubs = (nBsubs+nIsubs+) 2xnLookahead 3036 "\${1}\${2} " (nInput*2)-1 "\${5+} \${6+}" 3037 "REPL" 3038 3039 ¦\${1}\${2} ¦\${3}\${4} ¦REPL¦\${5+} \${6+}¦\${7+} \${8+}¦ 3040 3041 3042 INPUT nInput = 5 3043 ============================================================ 3044 ¦(0612)¦(ign) (0613)¦(ign) (0614)¦(ign) (0615)¦(ign) (0615)¦ 3045 \${1} \${2} \${3} \${4} \${5} \${6} \${7} \${8} \${9} (All backreference numbers are + nBsubs) 3046 ----- ------------ ------------ ------------ ------------ 3047 Input 1 Input 2 Input 3 Input 4 Input 5 3048 3049 A====== SequenceIndex=1 ; Lookup match nGlyphs=1 3050 B=================== SequenceIndex=1 ; Lookup match nGlyphs=2 3051 C=============================== SequenceIndex=1 ; Lookup match nGlyphs=3 3052 D======================= SequenceIndex=2 ; Lookup match nGlyphs=2 3053 E===================================== SequenceIndex=2 ; Lookup match nGlyphs=3 3054 F====================== SequenceIndex=4 ; Lookup match nGlyphs=2 3055 3056 All backreference numbers are + nBsubs 3057 A - "REPL\${2} \${3}\${4} \${5}\${6} \${7}\${8} \${9}" 3058 B - "REPL\${2}\${4} \${5}\${6} \${7}\${8} \${9}" 3059 C - "REPL\${2}\${4}\${6} \${7}\${8} \${9}" 3060 D - "\${1} REPL\${2}\${4}\${6} \${7}\${8} \${9}" 3061 E - "\${1} REPL\${2}\${4}\${6}\${8} \${9}" 3062 F - "\${1}\${2} \${3}\${4} \${5} REPL\${6}\${8}" 3063 */ 3064 3065 function _makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex) 3066 { 3067 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3068 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 3069 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context 3070 // $lookupGlyphs = array of glyphs (single Glyphs) making up Lookup Input sequence 3071 $mLen = count($lookupGlyphs); // nGlyphs in the secondary Lookup match 3072 $nInput = count($inputGlyphs); // nGlyphs in the Primary Input sequence 3073 $str = ""; 3074 for ($i = 0; $i < $nInput; $i++) { 3075 if ($i > 0) { 3076 $str .= $ignore . " "; 3077 } 3078 if ($i >= $seqIndex && $i < ($seqIndex + $mLen)) { 3079 $str .= "(" . $lookupGlyphs[($i - $seqIndex)] . ")"; 3080 } else { 3081 $str .= "(" . $inputGlyphs[($i)] . ")"; 3082 } 3083 } 3084 3085 return $str; 3086 } 3087 3088 function _makeGSUBinputMatch($inputGlyphs, $ignore) 3089 { 3090 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3091 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 3092 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context 3093 // $lookupGlyphs = array of glyphs making up Lookup Input sequence - if applicable 3094 $str = ""; 3095 for ($i = 1; $i <= count($inputGlyphs); $i++) { 3096 if ($i > 1) { 3097 $str .= $ignore . " "; 3098 } 3099 $str .= "(" . $inputGlyphs[($i - 1)] . ")"; 3100 } 3101 3102 return $str; 3103 } 3104 3105 function _makeGSUBbacktrackMatch($backtrackGlyphs, $ignore) 3106 { 3107 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3108 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 3109 // $backtrackGlyphs = array of glyphstrings making up Backtrack sequence 3110 // 3 2 1 0 3111 // each item being e.g. E0AD|E0AF|F1FD 3112 $str = ""; 3113 for ($i = (count($backtrackGlyphs) - 1); $i >= 0; $i--) { 3114 $str .= "(" . $backtrackGlyphs[$i] . ")" . $ignore . " "; 3115 } 3116 3117 return $str; 3118 } 3119 3120 function _makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore) 3121 { 3122 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3123 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 3124 // $lookaheadGlyphs = array of glyphstrings making up Lookahead sequence 3125 // 0 1 2 3 3126 // each item being e.g. E0AD|E0AF|F1FD 3127 $str = ""; 3128 for ($i = 0; $i < count($lookaheadGlyphs); $i++) { 3129 $str .= $ignore . " (" . $lookaheadGlyphs[$i] . ")"; 3130 } 3131 3132 return $str; 3133 } 3134 3135 function _makeGSUBinputReplacement($nInput, $REPL, $ignore, $nBsubs, $mLen, $seqIndex) 3136 { 3137 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}" 3138 // $nInput nGlyphs in the Primary Input sequence 3139 // $REPL replacement glyphs from secondary lookup 3140 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3141 // $nBsubs Number of Backtrack substitutions (= 2x Number of Backtrack glyphs) 3142 // $mLen nGlyphs in the secondary Lookup match - if no secondary lookup, should=$nInput 3143 // $seqIndex Sequence Index to apply the secondary match 3144 if ($ignore == "()") { 3145 $ign = false; 3146 } else { 3147 $ign = true; 3148 } 3149 $str = ""; 3150 if ($nInput == 1) { 3151 $str = $REPL; 3152 } elseif ($nInput > 1) { 3153 if ($mLen == $nInput) { // whole string replaced 3154 $str = $REPL; 3155 if ($ign) { 3156 // for every nInput over 1, add another replacement backreference, to move IGNORES after replacement 3157 for ($x = 2; $x <= $nInput; $x++) { 3158 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3159 } 3160 } 3161 } else { // if only part of string replaced: 3162 for ($x = 1; $x < ($seqIndex + 1); $x++) { 3163 if ($x == 1) { 3164 $str .= '\\' . ($nBsubs + 1); 3165 } else { 3166 if ($ign) { 3167 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3168 } 3169 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1))); 3170 } 3171 } 3172 if ($seqIndex > 0) { 3173 $str .= " "; 3174 } 3175 $str .= $REPL; 3176 if ($ign) { 3177 for ($x = (max(($seqIndex + 1), 2)); $x < ($seqIndex + 1 + $mLen); $x++) { // move IGNORES after replacement 3178 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3179 } 3180 } 3181 for ($x = ($seqIndex + 1 + $mLen); $x <= $nInput; $x++) { 3182 if ($ign) { 3183 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3184 } 3185 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1))); 3186 } 3187 } 3188 } 3189 3190 return $str; 3191 } 3192 3193 function _getCoverage($convert2hex = true, $mode = 1) 3194 { 3195 $g = []; 3196 $ctr = 0; 3197 $CoverageFormat = $this->read_ushort(); 3198 if ($CoverageFormat == 1) { 3199 $CoverageGlyphCount = $this->read_ushort(); 3200 for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) { 3201 $glyphID = $this->read_ushort(); 3202 $uni = $this->glyphToChar[$glyphID][0]; 3203 if ($convert2hex) { 3204 $g[] = unicode_hex($uni); 3205 } elseif ($mode == 2) { 3206 $g[$uni] = $ctr; 3207 $ctr++; 3208 } else { 3209 $g[] = $glyphID; 3210 } 3211 } 3212 } 3213 if ($CoverageFormat == 2) { 3214 $RangeCount = $this->read_ushort(); 3215 for ($r = 0; $r < $RangeCount; $r++) { 3216 $start = $this->read_ushort(); 3217 $end = $this->read_ushort(); 3218 $StartCoverageIndex = $this->read_ushort(); // n/a 3219 for ($glyphID = $start; $glyphID <= $end; $glyphID++) { 3220 $uni = $this->glyphToChar[$glyphID][0]; 3221 if ($convert2hex) { 3222 $g[] = unicode_hex($uni); 3223 } elseif ($mode == 2) { 3224 $uni = $g[$uni] = $ctr; 3225 $ctr++; 3226 } else { 3227 $g[] = $glyphID; 3228 } 3229 } 3230 } 3231 } 3232 3233 return $g; 3234 } 3235 3236 function _getClasses($offset) 3237 { 3238 $this->seek($offset); 3239 $ClassFormat = $this->read_ushort(); 3240 $GlyphByClass = []; 3241 if ($ClassFormat == 1) { 3242 $StartGlyph = $this->read_ushort(); 3243 $GlyphCount = $this->read_ushort(); 3244 for ($i = 0; $i < $GlyphCount; $i++) { 3245 $startGlyphID = $StartGlyph + $i; 3246 $endGlyphID = $StartGlyph + $i; 3247 $class = $this->read_ushort(); 3248 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) { 3249 if (isset($this->glyphToChar[$g][0])) { 3250 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]); 3251 } 3252 } 3253 } 3254 } elseif ($ClassFormat == 2) { 3255 $tableCount = $this->read_ushort(); 3256 for ($i = 0; $i < $tableCount; $i++) { 3257 $startGlyphID = $this->read_ushort(); 3258 $endGlyphID = $this->read_ushort(); 3259 $class = $this->read_ushort(); 3260 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) { 3261 if ($this->glyphToChar[$g][0]) { 3262 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]); 3263 } 3264 } 3265 } 3266 } 3267 $gbc = []; 3268 foreach ($GlyphByClass as $class => $garr) { 3269 $gbc[$class] = implode('|', $garr); 3270 } 3271 3272 return $gbc; 3273 } 3274 3275 function _getGPOStables() 3276 { 3277 /////////////////////////////////// 3278 // GPOS - Glyph Positioning 3279 /////////////////////////////////// 3280 if (!isset($this->tables["GPOS"])) { 3281 return [[], [], []]; 3282 } 3283 3284 $ffeats = []; 3285 $gpos_offset = $this->seek_table("GPOS"); 3286 $this->skip(4); 3287 $ScriptList_offset = $gpos_offset + $this->read_ushort(); 3288 $FeatureList_offset = $gpos_offset + $this->read_ushort(); 3289 $LookupList_offset = $gpos_offset + $this->read_ushort(); 3290 3291 // ScriptList 3292 $this->seek($ScriptList_offset); 3293 $ScriptCount = $this->read_ushort(); 3294 for ($i = 0; $i < $ScriptCount; $i++) { 3295 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc. 3296 $ScriptTableOffset = $this->read_ushort(); 3297 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset; 3298 } 3299 3300 // Script Table 3301 foreach ($ffeats as $t => $o) { 3302 $ls = []; 3303 $this->seek($o); 3304 $DefLangSys_offset = $this->read_ushort(); 3305 if ($DefLangSys_offset > 0) { 3306 $ls['DFLT'] = $DefLangSys_offset + $o; 3307 } 3308 $LangSysCount = $this->read_ushort(); 3309 for ($i = 0; $i < $LangSysCount; $i++) { 3310 $LangTag = $this->read_tag(); // = 3311 $LangTableOffset = $this->read_ushort(); 3312 $ls[$LangTag] = $o + $LangTableOffset; 3313 } 3314 $ffeats[$t] = $ls; 3315 } 3316 3317 // Get FeatureIndexList 3318 // LangSys Table - from first listed langsys 3319 foreach ($ffeats as $st => $scripts) { 3320 foreach ($scripts as $t => $o) { 3321 $FeatureIndex = []; 3322 $langsystable_offset = $o; 3323 $this->seek($langsystable_offset); 3324 $LookUpOrder = $this->read_ushort(); //==NULL 3325 $ReqFeatureIndex = $this->read_ushort(); 3326 if ($ReqFeatureIndex != 0xFFFF) { 3327 $FeatureIndex[] = $ReqFeatureIndex; 3328 } 3329 $FeatureCount = $this->read_ushort(); 3330 for ($i = 0; $i < $FeatureCount; $i++) { 3331 $FeatureIndex[] = $this->read_ushort(); // = index of feature 3332 } 3333 $ffeats[$st][$t] = $FeatureIndex; 3334 } 3335 } 3336 // Feauture List => LookupListIndex es 3337 $this->seek($FeatureList_offset); 3338 $FeatureCount = $this->read_ushort(); 3339 $Feature = []; 3340 for ($i = 0; $i < $FeatureCount; $i++) { 3341 $tag = $this->read_tag(); 3342 if ($tag === 'kern') { 3343 $this->haskernGPOS = true; 3344 } 3345 $Feature[$i] = ['tag' => $tag]; 3346 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort(); 3347 } 3348 3349 for ($i = 0; $i < $FeatureCount; $i++) { 3350 $this->seek($Feature[$i]['offset']); 3351 $this->read_ushort(); // null 3352 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort(); 3353 $Feature[$i]['LookupListIndex'] = []; 3354 for ($c = 0; $c < $Lookupcount; $c++) { 3355 $Feature[$i]['LookupListIndex'][] = $this->read_ushort(); 3356 } 3357 } 3358 3359 foreach ($ffeats as $st => $scripts) { 3360 foreach ($scripts as $t => $o) { 3361 $FeatureIndex = $ffeats[$st][$t]; 3362 foreach ($FeatureIndex as $k => $fi) { 3363 $ffeats[$st][$t][$k] = $Feature[$fi]; 3364 } 3365 } 3366 } 3367 3368 $gpos = []; 3369 $GPOSScriptLang = []; 3370 foreach ($ffeats as $st => $scripts) { 3371 3372 foreach ($scripts as $t => $langsys) { 3373 3374 $lg = []; 3375 foreach ($langsys as $ft) { 3376 $lg[$ft['LookupListIndex'][0]] = $ft; 3377 } 3378 3379 // list of Lookups in order they need to be run i.e. order listed in Lookup table 3380 ksort($lg); 3381 foreach ($lg as $ft) { 3382 $gpos[$st][$t][$ft['tag']] = $ft['LookupListIndex']; 3383 } 3384 if (!isset($GPOSScriptLang[$st])) { 3385 $GPOSScriptLang[$st] = ''; 3386 } 3387 $GPOSScriptLang[$st] .= $t . ' '; 3388 } 3389 } 3390 3391 // Get metadata and offsets for whole Lookup List table 3392 $this->seek($LookupList_offset); 3393 $LookupCount = $this->read_ushort(); 3394 $Lookup = []; 3395 $Offsets = []; 3396 $SubtableCount = []; 3397 3398 for ($i = 0; $i < $LookupCount; $i++) { 3399 $Offsets[$i] = $LookupList_offset + $this->read_ushort(); 3400 } 3401 3402 for ($i = 0; $i < $LookupCount; $i++) { 3403 $this->seek($Offsets[$i]); 3404 $Lookup[$i]['Type'] = $this->read_ushort(); 3405 $Lookup[$i]['Flag'] = $flag = $this->read_ushort(); 3406 $Lookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort(); 3407 for ($c = 0; $c < $SubtableCount[$i]; $c++) { 3408 $Lookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort(); 3409 } 3410 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure 3411 if (($flag & 0x0010) === 0x0010) { 3412 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort(); 3413 } else { 3414 $Lookup[$i]['MarkFilteringSet'] = ''; 3415 } 3416 3417 // Lookup Type 9: Extension 3418 if ($Lookup[$i]['Type'] == 9) { 3419 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type 3420 for ($c = 0; $c < $SubtableCount[$i]; $c++) { 3421 $this->seek($Lookup[$i]['Subtables'][$c]); 3422 $ExtensionPosFormat = $this->read_ushort(); 3423 $type = $this->read_ushort(); 3424 $Lookup[$i]['Subtables'][$c] = $Lookup[$i]['Subtables'][$c] + $this->read_ulong(); 3425 } 3426 $Lookup[$i]['Type'] = $type; 3427 } 3428 } 3429 3430 // Process Whole LookupList - Get LuCoverage = Lookup coverage just for first glyph 3431 $this->LuCoverage = []; 3432 for ($i = 0; $i < $LookupCount; $i++) { 3433 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 3434 $this->seek($Lookup[$i]['Subtables'][$c]); 3435 $PosFormat = $this->read_ushort(); 3436 3437 if ($Lookup[$i]['Type'] == 7 && $PosFormat == 3) { 3438 $this->skip(4); 3439 } elseif ($Lookup[$i]['Type'] == 8 && $PosFormat == 3) { 3440 $BacktrackGlyphCount = $this->read_ushort(); 3441 $this->skip(2 * $BacktrackGlyphCount + 2); 3442 } 3443 // NB Coverage only looks at glyphs for position 1 (i.e. 7.3 and 8.3) // NEEDS TO READ ALL ******************** 3444 // NB For e.g. Type 4, this may be the Coverage for the Mark 3445 $Coverage = $Lookup[$i]['Subtables'][$c] + $this->read_ushort(); 3446 $this->seek($Coverage); 3447 $glyphs = $this->_getCoverage(false, 2); 3448 $this->LuCoverage[$i][$c] = $glyphs; 3449 } 3450 } 3451 3452 $this->fontCache->jsonWrite($this->fontkey . '.GPOSdata.json', $this->LuCoverage); 3453 3454 return [$GPOSScriptLang, $gpos, $Lookup]; 3455 } 3456 3457 function makeSubset($file, &$subset, $TTCfontID = 0, $debug = false, $useOTL = false) 3458 { 3459 $this->useOTL = $useOTL; 3460 $this->filename = $file; 3461 $this->fh = fopen($file, 'rb'); 3462 3463 if (!$this->fh) { 3464 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file %s', $file)); 3465 } 3466 3467 $this->_pos = 0; 3468 $this->charWidths = ''; 3469 $this->glyphPos = []; 3470 $this->charToGlyph = []; 3471 $this->tables = []; 3472 $this->otables = []; 3473 $this->ascent = 0; 3474 $this->descent = 0; 3475 $this->strikeoutSize = 0; 3476 $this->strikeoutPosition = 0; 3477 $this->numTTCFonts = 0; 3478 $this->TTCFonts = []; 3479 $this->skip(4); 3480 $this->maxUni = 0; 3481 3482 if ($TTCfontID > 0) { 3483 $this->version = $version = $this->read_ulong(); // TTC Header version now 3484 if (!in_array($version, [0x00010000, 0x00020000], true)) { 3485 throw new \Mpdf\Exception\FontException(sprintf('Error parsing TrueType Collection: version=%s - %s', $version, $file)); 3486 } 3487 $this->numTTCFonts = $this->read_ulong(); 3488 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 3489 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 3490 } 3491 $this->seek($this->TTCFonts[$TTCfontID]['offset']); 3492 $this->version = $version = $this->read_ulong(); // TTFont version again now 3493 } 3494 $this->readTableDirectory($debug); 3495 3496 // head - Font header table 3497 $this->seek_table('head'); 3498 $this->skip(50); 3499 $indexToLocFormat = $this->read_ushort(); 3500 $glyphDataFormat = $this->read_ushort(); 3501 3502 // hhea - Horizontal header table 3503 $this->seek_table('hhea'); 3504 $this->skip(32); 3505 $metricDataFormat = $this->read_ushort(); 3506 $orignHmetrics = $numberOfHMetrics = $this->read_ushort(); 3507 3508 // maxp - Maximum profile table 3509 $this->seek_table('maxp'); 3510 $this->skip(4); 3511 $numGlyphs = $this->read_ushort(); 3512 3513 // cmap - Character to glyph index mapping table 3514 $cmap_offset = $this->seek_table('cmap'); 3515 $this->skip(2); 3516 $cmapTableCount = $this->read_ushort(); 3517 $unicode_cmap_offset = 0; 3518 for ($i = 0; $i < $cmapTableCount; $i++) { 3519 $platformID = $this->read_ushort(); 3520 $encodingID = $this->read_ushort(); 3521 $offset = $this->read_ulong(); 3522 $save_pos = $this->_pos; 3523 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode 3524 $format = $this->get_ushort($cmap_offset + $offset); 3525 if ($format == 4) { 3526 $unicode_cmap_offset = $cmap_offset + $offset; 3527 break; 3528 } 3529 } 3530 $this->seek($save_pos); 3531 } 3532 3533 if (!$unicode_cmap_offset) { 3534 throw new \Mpdf\Exception\FontException(sprintf('Font "%s" does not have Unicode cmap (platform 3, encoding 1, format 4, or platform 0 [any encoding] format 4)', $this->filename)); 3535 } 3536 3537 $glyphToChar = []; 3538 $charToGlyph = []; 3539 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 3540 3541 // Map Unmapped glyphs - from $numGlyphs 3542 if ($useOTL) { 3543 $bctr = 0xE000; 3544 for ($gid = 1; $gid < $numGlyphs; $gid++) { 3545 if (!isset($glyphToChar[$gid])) { 3546 while (isset($charToGlyph[$bctr])) { 3547 $bctr++; 3548 } // Avoid overwriting a glyph already mapped in PUA 3549 if ($bctr > 0xF8FF) { 3550 throw new \Mpdf\Exception\FontException($file . " : WARNING - Font cannot map all included glyphs into Private Use Area U+E000 - U+F8FF; cannot use useOTL on this font"); 3551 } 3552 $glyphToChar[$gid][] = $bctr; 3553 $charToGlyph[$bctr] = $gid; 3554 $bctr++; 3555 } 3556 } 3557 } 3558 3559 $this->charToGlyph = $charToGlyph; 3560 $this->glyphToChar = $glyphToChar; 3561 3562 // hmtx - Horizontal metrics table 3563 $scale = 1; // not used 3564 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale); 3565 3566 // loca - Index to location 3567 $this->getLOCA($indexToLocFormat, $numGlyphs); 3568 3569 $subsetglyphs = [0 => 0, 1 => 1, 2 => 2]; 3570 $subsetCharToGlyph = []; 3571 foreach ($subset as $code) { 3572 if (isset($this->charToGlyph[$code])) { 3573 $subsetglyphs[$this->charToGlyph[$code]] = $code; // Old Glyph ID => Unicode 3574 $subsetCharToGlyph[$code] = $this->charToGlyph[$code]; // Unicode to old GlyphID 3575 } 3576 $this->maxUni = max($this->maxUni, $code); 3577 } 3578 3579 list($start, $dummy) = $this->get_table_pos('glyf'); 3580 3581 $glyphSet = []; 3582 ksort($subsetglyphs); 3583 $n = 0; 3584 $fsLastCharIndex = 0; // maximum Unicode index (character code) in this font, according to the cmap subtable for platform ID 3 and platform- specific encoding ID 0 or 1. 3585 foreach ($subsetglyphs as $originalGlyphIdx => $uni) { 3586 $fsLastCharIndex = max($fsLastCharIndex, $uni); 3587 $glyphSet[$originalGlyphIdx] = $n; // old glyphID to new glyphID 3588 $n++; 3589 } 3590 3591 $codeToGlyph = []; 3592 ksort($subsetCharToGlyph); 3593 foreach ($subsetCharToGlyph as $uni => $originalGlyphIdx) { 3594 $codeToGlyph[$uni] = $glyphSet[$originalGlyphIdx]; 3595 } 3596 $this->codeToGlyph = $codeToGlyph; 3597 3598 ksort($subsetglyphs); 3599 foreach ($subsetglyphs as $originalGlyphIdx => $uni) { 3600 $this->getGlyphs($originalGlyphIdx, $start, $glyphSet, $subsetglyphs); 3601 } 3602 3603 $numGlyphs = $numberOfHMetrics = count($subsetglyphs); 3604 3605 // name - table copied from the original 3606 // MS spec says that "Platform and encoding ID's in the name table should be consistent with those in the cmap table. 3607 // If they are not, the font will not load in Windows" 3608 // Doesn't seem to be a problem? 3609 $this->add('name', $this->get_table('name')); 3610 3611 // tables copied from the original 3612 $tags = ['cvt ', 'fpgm', 'prep', 'gasp']; 3613 foreach ($tags as $tag) { 3614 if (isset($this->tables[$tag])) { 3615 $this->add($tag, $this->get_table($tag)); 3616 } 3617 } 3618 3619 // post - PostScript 3620 if (isset($this->tables['post'])) { 3621 $opost = $this->get_table('post'); 3622 $post = "\x00\x03\x00\x00" . substr($opost, 4, 12) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; 3623 $this->add('post', $post); 3624 } 3625 3626 // Sort CID2GID map into segments of contiguous codes 3627 ksort($codeToGlyph); 3628 unset($codeToGlyph[0]); 3629 3630 $rangeid = 0; 3631 $range = []; 3632 $prevcid = -2; 3633 $prevglidx = -1; 3634 3635 // for each character 3636 foreach ($codeToGlyph as $cid => $glidx) { 3637 if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) { 3638 $range[$rangeid][] = $glidx; 3639 } else { 3640 // new range 3641 $rangeid = $cid; 3642 $range[$rangeid] = []; 3643 $range[$rangeid][] = $glidx; 3644 } 3645 $prevcid = $cid; 3646 $prevglidx = $glidx; 3647 } 3648 3649 // cmap - Character to glyph mapping 3650 $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF 3651 $searchRange = 1; 3652 $entrySelector = 0; 3653 3654 while ($searchRange * 2 <= $segCount) { 3655 $searchRange *= 2; 3656 ++$entrySelector; 3657 } 3658 3659 $searchRange *= 2; 3660 $rangeShift = $segCount * 2 - $searchRange; 3661 $length = 16 + (8 * $segCount) + ($numGlyphs + 1); 3662 $cmap = [ 3663 0, 3, // Index : version, number of encoding subtables 3664 0, 0, // Encoding Subtable : platform (UNI=0), encoding 0 3665 0, 28, // Encoding Subtable : offset (hi,lo) 3666 0, 3, // Encoding Subtable : platform (UNI=0), encoding 3 3667 0, 28, // Encoding Subtable : offset (hi,lo) 3668 3, 1, // Encoding Subtable : platform (MS=3), encoding 1 3669 0, 28, // Encoding Subtable : offset (hi,lo) 3670 4, $length, 0, // Format 4 Mapping subtable: format, length, language 3671 $segCount * 2, 3672 $searchRange, 3673 $entrySelector, 3674 $rangeShift, 3675 ]; 3676 3677 // endCode(s) 3678 foreach ($range as $start => $subrange) { 3679 $endCode = $start + (count($subrange) - 1); 3680 $cmap[] = $endCode; // endCode(s) 3681 } 3682 3683 $cmap[] = 0xFFFF; // endCode of last Segment 3684 $cmap[] = 0; // reservedPad 3685 3686 // startCode(s) 3687 foreach ($range as $start => $subrange) { 3688 $cmap[] = $start; // startCode(s) 3689 } 3690 3691 $cmap[] = 0xFFFF; // startCode of last Segment 3692 3693 // idDelta(s) 3694 foreach ($range as $start => $subrange) { 3695 $idDelta = -($start - $subrange[0]); 3696 $n += count($subrange); 3697 $cmap[] = $idDelta; // idDelta(s) 3698 } 3699 3700 $cmap[] = 1; // idDelta of last Segment 3701 // idRangeOffset(s) 3702 3703 foreach ($range as $subrange) { 3704 $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 3705 } 3706 3707 $cmap[] = 0; // idRangeOffset of last Segment 3708 foreach ($range as $subrange) { 3709 foreach ($subrange as $glidx) { 3710 $cmap[] = $glidx; 3711 } 3712 } 3713 3714 $cmap[] = 0; // Mapping for last character 3715 $cmapstr = ''; 3716 3717 foreach ($cmap as $cm) { 3718 $cmapstr .= pack('n', $cm); 3719 } 3720 $this->add('cmap', $cmapstr); 3721 3722 // glyf - Glyph data 3723 list($glyfOffset, $glyfLength) = $this->get_table_pos('glyf'); 3724 if ($glyfLength < $this->maxStrLenRead) { 3725 $glyphData = $this->get_table('glyf'); 3726 } 3727 3728 $offsets = []; 3729 $glyf = ''; 3730 $pos = 0; 3731 $hmtxstr = ''; 3732 $xMinT = 0; 3733 $yMinT = 0; 3734 $xMaxT = 0; 3735 $yMaxT = 0; 3736 $advanceWidthMax = 0; 3737 $minLeftSideBearing = 0; 3738 $minRightSideBearing = 0; 3739 $xMaxExtent = 0; 3740 $maxPoints = 0; // points in non-compound glyph 3741 $maxContours = 0; // contours in non-compound glyph 3742 $maxComponentPoints = 0; // points in compound glyph 3743 $maxComponentContours = 0; // contours in compound glyph 3744 $maxComponentElements = 0; // number of glyphs referenced at top level 3745 $maxComponentDepth = 0; // levels of recursion, set to 0 if font has only simple glyphs 3746 $this->glyphdata = []; 3747 3748 foreach ($subsetglyphs as $originalGlyphIdx => $uni) { 3749 // hmtx - Horizontal Metrics 3750 $hm = $this->getHMetric($orignHmetrics, $originalGlyphIdx); 3751 $hmtxstr .= $hm; 3752 3753 $offsets[] = $pos; 3754 $glyphPos = $this->glyphPos[$originalGlyphIdx]; 3755 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos; 3756 if ($glyfLength < $this->maxStrLenRead) { 3757 $data = substr($glyphData, $glyphPos, $glyphLen); 3758 } else { 3759 if ($glyphLen > 0) { 3760 $data = $this->get_chunk($glyfOffset + $glyphPos, $glyphLen); 3761 } else { 3762 $data = ''; 3763 } 3764 } 3765 3766 if ($glyphLen > 0) { 3767 if (_RECALC_PROFILE) { 3768 $xMin = $this->unpack_short(substr($data, 2, 2)); 3769 $yMin = $this->unpack_short(substr($data, 4, 2)); 3770 $xMax = $this->unpack_short(substr($data, 6, 2)); 3771 $yMax = $this->unpack_short(substr($data, 8, 2)); 3772 $xMinT = min($xMinT, $xMin); 3773 $yMinT = min($yMinT, $yMin); 3774 $xMaxT = max($xMaxT, $xMax); 3775 $yMaxT = max($yMaxT, $yMax); 3776 $aw = $this->unpack_short(substr($hm, 0, 2)); 3777 $lsb = $this->unpack_short(substr($hm, 2, 2)); 3778 $advanceWidthMax = max($advanceWidthMax, $aw); 3779 $minLeftSideBearing = min($minLeftSideBearing, $lsb); 3780 $minRightSideBearing = min($minRightSideBearing, ($aw - $lsb - ($xMax - $xMin))); 3781 $xMaxExtent = max($xMaxExtent, ($lsb + ($xMax - $xMin))); 3782 } 3783 $up = unpack("n", substr($data, 0, 2)); 3784 } 3785 if ($glyphLen > 2 && ($up[1] & (1 << 15))) { // If number of contours <= -1 i.e. composiste glyph 3786 $pos_in_glyph = 10; 3787 $flags = GlyphOperator::MORE; 3788 $nComponentElements = 0; 3789 while ($flags & GlyphOperator::MORE) { 3790 $nComponentElements += 1; // number of glyphs referenced at top level 3791 $up = unpack("n", substr($data, $pos_in_glyph, 2)); 3792 $flags = $up[1]; 3793 $up = unpack("n", substr($data, $pos_in_glyph + 2, 2)); 3794 $glyphIdx = $up[1]; 3795 $this->glyphdata[$originalGlyphIdx]['compGlyphs'][] = $glyphIdx; 3796 $data = $this->_set_ushort($data, $pos_in_glyph + 2, $glyphSet[$glyphIdx]); 3797 $pos_in_glyph += 4; 3798 if ($flags & GlyphOperator::WORDS) { 3799 $pos_in_glyph += 4; 3800 } else { 3801 $pos_in_glyph += 2; 3802 } 3803 if ($flags & GlyphOperator::SCALE) { 3804 $pos_in_glyph += 2; 3805 } elseif ($flags & GlyphOperator::XYSCALE) { 3806 $pos_in_glyph += 4; 3807 } elseif ($flags & GlyphOperator::TWOBYTWO) { 3808 $pos_in_glyph += 8; 3809 } 3810 } 3811 $maxComponentElements = max($maxComponentElements, $nComponentElements); 3812 3813 } // Simple Glyph 3814 elseif (_RECALC_PROFILE && $glyphLen > 2 && $up[1] < (1 << 15) && $up[1] > 0) { // Number of contours > 0 simple glyph 3815 $nContours = $up[1]; 3816 $this->glyphdata[$originalGlyphIdx]['nContours'] = $nContours; 3817 $maxContours = max($maxContours, $nContours); 3818 3819 // Count number of points in simple glyph 3820 $pos_in_glyph = 10 + ($nContours * 2) - 2; // Last endContourPoint 3821 $up = unpack("n", substr($data, $pos_in_glyph, 2)); 3822 $points = $up[1] + 1; 3823 $this->glyphdata[$originalGlyphIdx]['nPoints'] = $points; 3824 $maxPoints = max($maxPoints, $points); 3825 } 3826 3827 $glyf .= $data; 3828 $pos += $glyphLen; 3829 if ($pos % 4 != 0) { 3830 $padding = 4 - ($pos % 4); 3831 $glyf .= str_repeat("\0", $padding); 3832 $pos += $padding; 3833 } 3834 } 3835 3836 if (_RECALC_PROFILE) { 3837 foreach ($this->glyphdata as $originalGlyphIdx => $val) { 3838 $maxdepth = $depth = -1; 3839 $points = 0; 3840 $contours = 0; 3841 $this->getGlyphData($originalGlyphIdx, $maxdepth, $depth, $points, $contours); 3842 $maxComponentDepth = max($maxComponentDepth, $maxdepth); 3843 $maxComponentPoints = max($maxComponentPoints, $points); 3844 $maxComponentContours = max($maxComponentContours, $contours); 3845 } 3846 } 3847 3848 $offsets[] = $pos; 3849 $this->add('glyf', $glyf); 3850 3851 // hmtx - Horizontal Metrics 3852 $this->add('hmtx', $hmtxstr); 3853 3854 // loca - Index to location 3855 $locastr = ''; 3856 if ((($pos + 1) >> 1) > 0xFFFF) { 3857 $indexToLocFormat = 1; // long format 3858 foreach ($offsets as $offset) { 3859 $locastr .= pack("N", $offset); 3860 } 3861 } else { 3862 $indexToLocFormat = 0; // short format 3863 foreach ($offsets as $offset) { 3864 $locastr .= pack("n", ($offset / 2)); 3865 } 3866 } 3867 $this->add('loca', $locastr); 3868 3869 // head - Font header 3870 $head = $this->get_table('head'); 3871 $head = $this->_set_ushort($head, 50, $indexToLocFormat); 3872 3873 if (_RECALC_PROFILE) { 3874 $head = $this->_set_short($head, 36, $xMinT); // for all glyph bounding boxes 3875 $head = $this->_set_short($head, 38, $yMinT); // for all glyph bounding boxes 3876 $head = $this->_set_short($head, 40, $xMaxT); // for all glyph bounding boxes 3877 $head = $this->_set_short($head, 42, $yMaxT); // for all glyph bounding boxes 3878 $head[17] = chr($head[17] & ~(1 << 4)); // Unset Bit 4 (as hdmx/LTSH tables not included) 3879 } 3880 3881 $this->add('head', $head); 3882 3883 // hhea - Horizontal Header 3884 $hhea = $this->get_table('hhea'); 3885 $hhea = $this->_set_ushort($hhea, 34, $numberOfHMetrics); 3886 if (_RECALC_PROFILE) { 3887 $hhea = $this->_set_ushort($hhea, 10, $advanceWidthMax); 3888 $hhea = $this->_set_short($hhea, 12, $minLeftSideBearing); 3889 $hhea = $this->_set_short($hhea, 14, $minRightSideBearing); 3890 $hhea = $this->_set_short($hhea, 16, $xMaxExtent); 3891 } 3892 $this->add('hhea', $hhea); 3893 3894 // maxp - Maximum Profile 3895 $maxp = $this->get_table('maxp'); 3896 $maxp = $this->_set_ushort($maxp, 4, $numGlyphs); 3897 if (_RECALC_PROFILE) { 3898 $maxp = $this->_set_ushort($maxp, 6, $maxPoints); // points in non-compound glyph 3899 $maxp = $this->_set_ushort($maxp, 8, $maxContours); // contours in non-compound glyph 3900 $maxp = $this->_set_ushort($maxp, 10, $maxComponentPoints); // points in compound glyph 3901 $maxp = $this->_set_ushort($maxp, 12, $maxComponentContours); // contours in compound glyph 3902 $maxp = $this->_set_ushort($maxp, 28, $maxComponentElements); // number of glyphs referenced at top level 3903 $maxp = $this->_set_ushort($maxp, 30, $maxComponentDepth); // levels of recursion, set to 0 if font has only simple glyphs 3904 } 3905 $this->add('maxp', $maxp); 3906 3907 // OS/2 - OS/2 3908 if (isset($this->tables['OS/2'])) { 3909 $os2_offset = $this->seek_table("OS/2"); 3910 if (_RECALC_PROFILE) { 3911 $fsSelection = $this->get_ushort($os2_offset + 62); 3912 $fsSelection = ($fsSelection & ~(1 << 6)); // 2-byte bit field containing information concerning the nature of the font patterns 3913 // bit#0 = Italic; bit#5=Bold 3914 // Match name table's font subfamily string 3915 // Clear bit#6 used for 'Regular' and optional 3916 } 3917 3918 // NB Currently this method never subsets characters above BMP 3919 // Could set nonBMP bit according to $this->maxUni 3920 $nonBMP = $this->get_ushort($os2_offset + 46); 3921 $nonBMP = ($nonBMP & ~(1 << 9)); // Unset Bit 57 (indicates non-BMP) - for interactive forms 3922 3923 $os2 = $this->get_table('OS/2'); 3924 if (_RECALC_PROFILE) { 3925 $os2 = $this->_set_ushort($os2, 62, $fsSelection); 3926 $os2 = $this->_set_ushort($os2, 66, $fsLastCharIndex); 3927 $os2 = $this->_set_ushort($os2, 42, 0x0000); // ulCharRange (ulUnicodeRange) bits 24-31 | 16-23 3928 $os2 = $this->_set_ushort($os2, 44, 0x0000); // ulCharRange (Unicode ranges) bits 8-15 | 0-7 3929 $os2 = $this->_set_ushort($os2, 46, $nonBMP); // ulCharRange (Unicode ranges) bits 56-63 | 48-55 3930 $os2 = $this->_set_ushort($os2, 48, 0x0000); // ulCharRange (Unicode ranges) bits 40-47 | 32-39 3931 $os2 = $this->_set_ushort($os2, 50, 0x0000); // ulCharRange (Unicode ranges) bits 88-95 | 80-87 3932 $os2 = $this->_set_ushort($os2, 52, 0x0000); // ulCharRange (Unicode ranges) bits 72-79 | 64-71 3933 $os2 = $this->_set_ushort($os2, 54, 0x0000); // ulCharRange (Unicode ranges) bits 120-127 | 112-119 3934 $os2 = $this->_set_ushort($os2, 56, 0x0000); // ulCharRange (Unicode ranges) bits 104-111 | 96-103 3935 } 3936 $os2 = $this->_set_ushort($os2, 46, $nonBMP); // Unset Bit 57 (indicates non-BMP) - for interactive forms 3937 3938 $this->add('OS/2', $os2); 3939 } 3940 3941 fclose($this->fh); 3942 3943 // Put the TTF file together 3944 $stm = ''; 3945 $this->endTTFile($stm); 3946 3947 return $stm; 3948 } 3949 3950 function makeSubsetSIP($file, &$subset, $TTCfontID = 0, $debug = false, $useOTL = 0) 3951 { 3952 $this->fh = fopen($file, 'rb'); 3953 3954 if (!$this->fh) { 3955 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file "%s"', $file)); 3956 } 3957 3958 $this->filename = $file; 3959 $this->_pos = 0; 3960 $this->useOTL = $useOTL; // mPDF 5.7.1 3961 $this->charWidths = ''; 3962 $this->glyphPos = []; 3963 $this->charToGlyph = []; 3964 $this->tables = []; 3965 $this->otables = []; 3966 $this->ascent = 0; 3967 $this->descent = 0; 3968 $this->strikeoutSize = 0; 3969 $this->strikeoutPosition = 0; 3970 $this->numTTCFonts = 0; 3971 $this->TTCFonts = []; 3972 $this->skip(4); 3973 3974 if ($TTCfontID > 0) { 3975 $this->version = $version = $this->read_ulong(); // TTC Header version now 3976 if (!in_array($version, [0x00010000, 0x00020000])) { 3977 throw new \Mpdf\Exception\FontException("ERROR - Error parsing TrueType Collection: version=" . $version . " - " . $file); 3978 } 3979 $this->numTTCFonts = $this->read_ulong(); 3980 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 3981 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 3982 } 3983 $this->seek($this->TTCFonts[$TTCfontID]['offset']); 3984 $this->version = $version = $this->read_ulong(); // TTFont version again now 3985 } 3986 $this->readTableDirectory($debug); 3987 3988 // head - Font header table 3989 $this->seek_table('head'); 3990 $this->skip(50); 3991 $indexToLocFormat = $this->read_ushort(); 3992 $glyphDataFormat = $this->read_ushort(); 3993 3994 // hhea - Horizontal header table 3995 $this->seek_table('hhea'); 3996 $this->skip(32); 3997 $metricDataFormat = $this->read_ushort(); 3998 $orignHmetrics = $numberOfHMetrics = $this->read_ushort(); 3999 4000 // maxp - Maximum profile table 4001 $this->seek_table('maxp'); 4002 $this->skip(4); 4003 $numGlyphs = $this->read_ushort(); 4004 4005 // cmap - Character to glyph index mapping table 4006 $cmap_offset = $this->seek_table('cmap'); 4007 $this->skip(2); 4008 $cmapTableCount = $this->read_ushort(); 4009 $unicode_cmap_offset = 0; 4010 for ($i = 0; $i < $cmapTableCount; $i++) { 4011 4012 $platformID = $this->read_ushort(); 4013 $encodingID = $this->read_ushort(); 4014 $offset = $this->read_ulong(); 4015 $save_pos = $this->_pos; 4016 4017 if (($platformID == 3 && $encodingID == 10) || $platformID == 0) { // Microsoft, Unicode Format 12 table HKCS 4018 $format = $this->get_ushort($cmap_offset + $offset); 4019 if ($format == 12) { 4020 $unicode_cmap_offset = $cmap_offset + $offset; 4021 break; 4022 } 4023 } 4024 4025 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode 4026 $format = $this->get_ushort($cmap_offset + $offset); 4027 if ($format == 4) { 4028 $unicode_cmap_offset = $cmap_offset + $offset; 4029 } 4030 } 4031 4032 $this->seek($save_pos); 4033 } 4034 4035 if (!$unicode_cmap_offset) { 4036 throw new \Mpdf\Exception\FontException(sprintf('Font "%s" does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)', $file)); 4037 } 4038 4039 // Format 12 CMAP does characters above Unicode BMP i.e. some HKCS characters U+20000 and above 4040 if ($format == 12) { 4041 $this->maxUniChar = 0; 4042 $this->seek($unicode_cmap_offset + 4); 4043 $length = $this->read_ulong(); 4044 $limit = $unicode_cmap_offset + $length; 4045 $this->skip(4); 4046 4047 $nGroups = $this->read_ulong(); 4048 4049 $glyphToChar = []; 4050 $charToGlyph = []; 4051 for ($i = 0; $i < $nGroups; $i++) { 4052 $startCharCode = $this->read_ulong(); 4053 $endCharCode = $this->read_ulong(); 4054 $startGlyphCode = $this->read_ulong(); 4055 $offset = 0; 4056 for ($unichar = $startCharCode; $unichar <= $endCharCode; $unichar++) { 4057 $glyph = $startGlyphCode + $offset; 4058 $offset++; 4059 // ZZZ98 4060 if ($unichar < 0x30000) { 4061 $charToGlyph[$unichar] = $glyph; 4062 $this->maxUniChar = max($unichar, $this->maxUniChar); 4063 $glyphToChar[$glyph][] = $unichar; 4064 } 4065 } 4066 } 4067 } else { 4068 $glyphToChar = []; 4069 $charToGlyph = []; 4070 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 4071 } 4072 4073 // Map Unmapped glyphs - from $numGlyphs 4074 if ($useOTL) { 4075 $bctr = 0xE000; 4076 for ($gid = 1; $gid < $numGlyphs; $gid++) { 4077 if (!isset($glyphToChar[$gid])) { 4078 while (isset($charToGlyph[$bctr])) { 4079 $bctr++; 4080 } // Avoid overwriting a glyph already mapped in PUA 4081 // ZZZ98 4082 if ($bctr > 0xF8FF && $bctr < 0x2CEB0) { 4083 $bctr = 0x2CEB0; 4084 while (isset($charToGlyph[$bctr])) { 4085 $bctr++; 4086 } 4087 } 4088 $glyphToChar[$gid][] = $bctr; 4089 $charToGlyph[$bctr] = $gid; 4090 $this->maxUniChar = max($bctr, $this->maxUniChar); 4091 $bctr++; 4092 } 4093 } 4094 } 4095 4096 // hmtx - Horizontal metrics table 4097 $scale = 1; // not used here 4098 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale); 4099 4100 // loca - Index to location 4101 $this->getLOCA($indexToLocFormat, $numGlyphs); 4102 4103 $glyphMap = [0 => 0]; 4104 $glyphSet = [0 => 0]; 4105 $codeToGlyph = []; 4106 4107 // Set a substitute if ASCII characters do not have glyphs 4108 if (isset($charToGlyph[0x3F])) { 4109 $subs = $charToGlyph[0x3F]; 4110 } else { // Question mark 4111 $subs = $charToGlyph[32]; 4112 } 4113 4114 foreach ($subset as $code) { 4115 if (isset($charToGlyph[$code])) { 4116 $originalGlyphIdx = $charToGlyph[$code]; 4117 } elseif ($code < 128) { 4118 $originalGlyphIdx = $subs; 4119 } else { 4120 $originalGlyphIdx = 0; 4121 } 4122 if (!isset($glyphSet[$originalGlyphIdx])) { 4123 $glyphSet[$originalGlyphIdx] = count($glyphMap); 4124 $glyphMap[] = $originalGlyphIdx; 4125 } 4126 $codeToGlyph[$code] = $glyphSet[$originalGlyphIdx]; 4127 } 4128 4129 list($start, $dummy) = $this->get_table_pos('glyf'); 4130 4131 $n = 0; 4132 while ($n < count($glyphMap)) { 4133 $originalGlyphIdx = $glyphMap[$n]; 4134 $glyphPos = $this->glyphPos[$originalGlyphIdx]; 4135 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos; 4136 ++$n; 4137 if (!$glyphLen) { 4138 continue; 4139 } 4140 $this->seek($start + $glyphPos); 4141 $numberOfContours = $this->read_short(); 4142 if ($numberOfContours < 0) { 4143 $this->skip(8); 4144 $flags = GlyphOperator::MORE; 4145 while ($flags & GlyphOperator::MORE) { 4146 $flags = $this->read_ushort(); 4147 $glyphIdx = $this->read_ushort(); 4148 if (!isset($glyphSet[$glyphIdx])) { 4149 $glyphSet[$glyphIdx] = count($glyphMap); 4150 $glyphMap[] = $glyphIdx; 4151 } 4152 if ($flags & GlyphOperator::WORDS) { 4153 $this->skip(4); 4154 } else { 4155 $this->skip(2); 4156 } 4157 if ($flags & GlyphOperator::SCALE) { 4158 $this->skip(2); 4159 } elseif ($flags & GlyphOperator::XYSCALE) { 4160 $this->skip(4); 4161 } elseif ($flags & GlyphOperator::TWOBYTWO) { 4162 $this->skip(8); 4163 } 4164 } 4165 } 4166 } 4167 4168 $numGlyphs = $n = count($glyphMap); 4169 $numberOfHMetrics = $n; 4170 4171 // MS spec says that "Platform and encoding ID's in the name table should be consistent with those in the cmap table. 4172 // If they are not, the font will not load in Windows" 4173 // Doesn't seem to be a problem? 4174 // Needs to have a name entry in 3,0 (e.g. symbol) - original font will be 3,1 (i.e. Unicode) 4175 $name = $this->get_table('name'); 4176 $name_offset = $this->seek_table("name"); 4177 $format = $this->read_ushort(); 4178 $numRecords = $this->read_ushort(); 4179 $string_data_offset = $name_offset + $this->read_ushort(); 4180 for ($i = 0; $i < $numRecords; $i++) { 4181 $platformId = $this->read_ushort(); 4182 $encodingId = $this->read_ushort(); 4183 if ($platformId == 3 && $encodingId == 1) { 4184 $pos = 6 + ($i * 12) + 2; 4185 $name = $this->_set_ushort($name, $pos, 0x00); // Change encoding to 3,0 rather than 3,1 4186 } 4187 $this->skip(8); 4188 } 4189 $this->add('name', $name); 4190 4191 // OS/2 4192 if (isset($this->tables['OS/2'])) { 4193 $os2 = $this->get_table('OS/2'); 4194 $os2 = $this->_set_ushort($os2, 42, 0x00); // ulCharRange (Unicode ranges) 4195 $os2 = $this->_set_ushort($os2, 44, 0x00); // ulCharRange (Unicode ranges) 4196 $os2 = $this->_set_ushort($os2, 46, 0x00); // ulCharRange (Unicode ranges) 4197 $os2 = $this->_set_ushort($os2, 48, 0x00); // ulCharRange (Unicode ranges) 4198 4199 $os2 = $this->_set_ushort($os2, 50, 0x00); // ulCharRange (Unicode ranges) 4200 $os2 = $this->_set_ushort($os2, 52, 0x00); // ulCharRange (Unicode ranges) 4201 $os2 = $this->_set_ushort($os2, 54, 0x00); // ulCharRange (Unicode ranges) 4202 $os2 = $this->_set_ushort($os2, 56, 0x00); // ulCharRange (Unicode ranges) 4203 // Set Symbol character only in ulCodePageRange 4204 $os2 = $this->_set_ushort($os2, 78, 0x8000); // ulCodePageRange = Bit #31 Symbol **** 78 = Bit 16-31 4205 $os2 = $this->_set_ushort($os2, 80, 0x0000); // ulCodePageRange = Bit #31 Symbol **** 80 = Bit 0-15 4206 $os2 = $this->_set_ushort($os2, 82, 0x0000); // ulCodePageRange = Bit #32- Symbol **** 82 = Bits 48-63 4207 $os2 = $this->_set_ushort($os2, 84, 0x0000); // ulCodePageRange = Bit #32- Symbol **** 84 = Bits 32-47 4208 4209 $os2 = $this->_set_ushort($os2, 64, 0x01); // FirstCharIndex 4210 $os2 = $this->_set_ushort($os2, 66, count($subset)); // LastCharIndex 4211 // Set PANOSE first bit to 5 for Symbol 4212 $os2 = $this->splice($os2, 32, chr(5) . chr(0) . chr(1) . chr(0) . chr(1) . chr(0) . chr(0) . chr(0) . chr(0) . chr(0)); 4213 $this->add('OS/2', $os2); 4214 } 4215 4216 //tables copied from the original 4217 $tags = ['cvt ', 'fpgm', 'prep', 'gasp']; 4218 foreach ($tags as $tag) { // 1.02 4219 if (isset($this->tables[$tag])) { 4220 $this->add($tag, $this->get_table($tag)); 4221 } 4222 } 4223 4224 // post - PostScript 4225 if (isset($this->tables['post'])) { 4226 $opost = $this->get_table('post'); 4227 $post = "\x00\x03\x00\x00" . substr($opost, 4, 12) . "\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00"; 4228 } 4229 $this->add('post', $post); 4230 4231 // hhea - Horizontal Header 4232 $hhea = $this->get_table('hhea'); 4233 $hhea = $this->_set_ushort($hhea, 34, $numberOfHMetrics); 4234 $this->add('hhea', $hhea); 4235 4236 // maxp - Maximum Profile 4237 $maxp = $this->get_table('maxp'); 4238 $maxp = $this->_set_ushort($maxp, 4, $numGlyphs); 4239 $this->add('maxp', $maxp); 4240 4241 // CMap table Formats [1,0,]6 and [3,0,]4 4242 // Sort CID2GID map into segments of contiguous codes 4243 $rangeid = 0; 4244 $range = []; 4245 $prevcid = -2; 4246 $prevglidx = -1; 4247 4248 // for each character 4249 foreach ($subset as $cid => $code) { 4250 $glidx = $codeToGlyph[$code]; 4251 if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) { 4252 $range[$rangeid][] = $glidx; 4253 } else { 4254 // new range 4255 $rangeid = $cid; 4256 $range[$rangeid] = []; 4257 $range[$rangeid][] = $glidx; 4258 } 4259 $prevcid = $cid; 4260 $prevglidx = $glidx; 4261 } 4262 4263 // cmap - Character to glyph mapping 4264 $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF 4265 $searchRange = 1; 4266 $entrySelector = 0; 4267 4268 while ($searchRange * 2 <= $segCount) { 4269 $searchRange = $searchRange * 2; 4270 $entrySelector = $entrySelector + 1; 4271 } 4272 4273 $searchRange = $searchRange * 2; 4274 $rangeShift = $segCount * 2 - $searchRange; 4275 $length = 16 + (8 * $segCount) + ($numGlyphs + 1); 4276 $cmap = [ 4277 4, $length, 0, // Format 4 Mapping subtable: format, length, language 4278 $segCount * 2, 4279 $searchRange, 4280 $entrySelector, 4281 $rangeShift, 4282 ]; 4283 4284 // endCode(s) 4285 foreach ($range as $start => $subrange) { 4286 $endCode = $start + (count($subrange) - 1); 4287 $cmap[] = $endCode; // endCode(s) 4288 } 4289 $cmap[] = 0xFFFF; // endCode of last Segment 4290 $cmap[] = 0; // reservedPad 4291 4292 // startCode(s) 4293 foreach ($range as $start => $subrange) { 4294 $cmap[] = $start; // startCode(s) 4295 } 4296 $cmap[] = 0xFFFF; // startCode of last Segment 4297 4298 // idDelta(s) 4299 foreach ($range as $start => $subrange) { 4300 $idDelta = -($start - $subrange[0]); 4301 $n += count($subrange); 4302 $cmap[] = $idDelta; // idDelta(s) 4303 } 4304 $cmap[] = 1; // idDelta of last Segment 4305 4306 // idRangeOffset(s) 4307 foreach ($range as $subrange) { 4308 $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 4309 } 4310 4311 $cmap[] = 0; // idRangeOffset of last Segment 4312 foreach ($range as $subrange) { 4313 foreach ($subrange as $glidx) { 4314 $cmap[] = $glidx; 4315 } 4316 } 4317 4318 $cmap[] = 0; // Mapping for last character 4319 $cmapstr4 = ''; 4320 foreach ($cmap as $cm) { 4321 $cmapstr4 .= pack("n", $cm); 4322 } 4323 4324 // cmap - Character to glyph mapping 4325 $entryCount = count($subset); 4326 $length = 10 + $entryCount * 2; 4327 4328 $off = 20 + $length; 4329 $hoff = $off >> 16; 4330 $loff = $off & 0xFFFF; 4331 4332 $cmap = [ 4333 0, 2, // Index : version, number of subtables 4334 1, 0, // Subtable : platform, encoding 4335 0, 20, // offset (hi,lo) 4336 3, 0, // Subtable : platform, encoding // See note above for 'name' 4337 $hoff, $loff, // offset (hi,lo) 4338 6, $length, // Format 6 Mapping table: format, length 4339 0, 1, // language, First char code 4340 $entryCount, 4341 ]; 4342 4343 $cmapstr = ''; 4344 foreach ($subset as $code) { 4345 $cmap[] = $codeToGlyph[$code]; 4346 } 4347 4348 foreach ($cmap as $cm) { 4349 $cmapstr .= pack("n", $cm); 4350 } 4351 4352 $cmapstr .= $cmapstr4; 4353 $this->add('cmap', $cmapstr); 4354 4355 // hmtx - Horizontal Metrics 4356 $hmtxstr = ''; 4357 for ($n = 0; $n < $numGlyphs; $n++) { 4358 $originalGlyphIdx = $glyphMap[$n]; 4359 $hm = $this->getHMetric($orignHmetrics, $originalGlyphIdx); 4360 $hmtxstr .= $hm; 4361 } 4362 $this->add('hmtx', $hmtxstr); 4363 4364 // glyf - Glyph data 4365 list($glyfOffset, $glyfLength) = $this->get_table_pos('glyf'); 4366 if ($glyfLength < $this->maxStrLenRead) { 4367 $glyphData = $this->get_table('glyf'); 4368 } 4369 4370 $offsets = []; 4371 $glyf = ''; 4372 $pos = 0; 4373 for ($n = 0; $n < $numGlyphs; $n++) { 4374 4375 $offsets[] = $pos; 4376 $originalGlyphIdx = $glyphMap[$n]; 4377 $glyphPos = $this->glyphPos[$originalGlyphIdx]; 4378 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos; 4379 4380 if ($glyfLength < $this->maxStrLenRead) { 4381 $data = substr($glyphData, $glyphPos, $glyphLen); 4382 } else { 4383 if ($glyphLen > 0) { 4384 $data = $this->get_chunk($glyfOffset + $glyphPos, $glyphLen); 4385 } else { 4386 $data = ''; 4387 } 4388 } 4389 4390 if ($glyphLen > 0) { 4391 $up = unpack('n', substr($data, 0, 2)); 4392 } 4393 4394 if ($glyphLen > 2 && ($up[1] & (1 << 15))) { 4395 4396 $pos_in_glyph = 10; 4397 $flags = GlyphOperator::MORE; 4398 4399 while ($flags & GlyphOperator::MORE) { 4400 $up = unpack('n', substr($data, $pos_in_glyph, 2)); 4401 $flags = $up[1]; 4402 $up = unpack('n', substr($data, $pos_in_glyph + 2, 2)); 4403 $glyphIdx = $up[1]; 4404 $data = $this->_set_ushort($data, $pos_in_glyph + 2, $glyphSet[$glyphIdx]); 4405 $pos_in_glyph += 4; 4406 4407 if ($flags & GlyphOperator::WORDS) { 4408 $pos_in_glyph += 4; 4409 } else { 4410 $pos_in_glyph += 2; 4411 } 4412 4413 if ($flags & GlyphOperator::SCALE) { 4414 $pos_in_glyph += 2; 4415 } elseif ($flags & GlyphOperator::XYSCALE) { 4416 $pos_in_glyph += 4; 4417 } elseif ($flags & GlyphOperator::TWOBYTWO) { 4418 $pos_in_glyph += 8; 4419 } 4420 } 4421 } 4422 4423 $glyf .= $data; 4424 $pos += $glyphLen; 4425 4426 if ($pos % 4 != 0) { 4427 $padding = 4 - ($pos % 4); 4428 $glyf .= str_repeat("\0", $padding); 4429 $pos += $padding; 4430 } 4431 } 4432 4433 $offsets[] = $pos; 4434 $this->add('glyf', $glyf); 4435 4436 // loca - Index to location 4437 $locastr = ''; 4438 if ((($pos + 1) >> 1) > 0xFFFF) { 4439 $indexToLocFormat = 1; // long format 4440 foreach ($offsets as $offset) { 4441 $locastr .= pack("N", $offset); 4442 } 4443 } else { 4444 $indexToLocFormat = 0; // short format 4445 foreach ($offsets as $offset) { 4446 $locastr .= pack("n", ($offset / 2)); 4447 } 4448 } 4449 4450 $this->add('loca', $locastr); 4451 4452 // head - Font header 4453 $head = $this->get_table('head'); 4454 $head = $this->_set_ushort($head, 50, $indexToLocFormat); 4455 $this->add('head', $head); 4456 4457 fclose($this->fh); 4458 4459 $stm = ''; 4460 $this->endTTFile($stm); 4461 4462 return $stm; 4463 } 4464 4465 function getGlyphData($originalGlyphIdx, &$maxdepth, &$depth, &$points, &$contours) 4466 { 4467 $depth++; 4468 $maxdepth = max($maxdepth, $depth); 4469 4470 if (count($this->glyphdata[$originalGlyphIdx]['compGlyphs'])) { 4471 foreach ($this->glyphdata[$originalGlyphIdx]['compGlyphs'] as $glyphIdx) { 4472 $this->getGlyphData($glyphIdx, $maxdepth, $depth, $points, $contours); 4473 } 4474 } elseif (($this->glyphdata[$originalGlyphIdx]['nContours'] > 0) && $depth > 0) { // simple 4475 $contours += $this->glyphdata[$originalGlyphIdx]['nContours']; 4476 $points += $this->glyphdata[$originalGlyphIdx]['nPoints']; 4477 } 4478 4479 $depth--; 4480 } 4481 4482 function getGlyphs($originalGlyphIdx, &$start, &$glyphSet, &$subsetglyphs) 4483 { 4484 $glyphPos = $this->glyphPos[$originalGlyphIdx]; 4485 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos; 4486 4487 if (!$glyphLen) { 4488 return; 4489 } 4490 4491 $this->seek($start + $glyphPos); 4492 $numberOfContours = $this->read_short(); 4493 4494 if ($numberOfContours < 0) { 4495 $this->skip(8); 4496 $flags = GlyphOperator::MORE; 4497 while ($flags & GlyphOperator::MORE) { 4498 $flags = $this->read_ushort(); 4499 $glyphIdx = $this->read_ushort(); 4500 if (!isset($glyphSet[$glyphIdx])) { 4501 $glyphSet[$glyphIdx] = count($subsetglyphs); // old glyphID to new glyphID 4502 $subsetglyphs[$glyphIdx] = true; 4503 } 4504 $savepos = ftell($this->fh); 4505 $this->getGlyphs($glyphIdx, $start, $glyphSet, $subsetglyphs); 4506 $this->seek($savepos); 4507 if ($flags & GlyphOperator::WORDS) { 4508 $this->skip(4); 4509 } else { 4510 $this->skip(2); 4511 } 4512 if ($flags & GlyphOperator::SCALE) { 4513 $this->skip(2); 4514 } elseif ($flags & GlyphOperator::XYSCALE) { 4515 $this->skip(4); 4516 } elseif ($flags & GlyphOperator::TWOBYTWO) { 4517 $this->skip(8); 4518 } 4519 } 4520 } 4521 } 4522 4523 function getHMTX($numberOfHMetrics, $numGlyphs, &$glyphToChar, $scale) 4524 { 4525 $start = $this->seek_table('hmtx'); 4526 $aw = 0; 4527 $this->charWidths = str_pad('', 256 * 256 * 2, "\x00"); 4528 4529 if ($this->maxUniChar > 65536) { 4530 $this->charWidths .= str_pad('', 256 * 256 * 2, "\x00"); 4531 } // Plane 1 SMP 4532 4533 if ($this->maxUniChar > 131072) { 4534 $this->charWidths .= str_pad('', 256 * 256 * 2, "\x00"); 4535 } // Plane 2 SMP 4536 4537 $nCharWidths = 0; 4538 if (($numberOfHMetrics * 4) < $this->maxStrLenRead) { 4539 $data = $this->get_chunk($start, $numberOfHMetrics * 4); 4540 $arr = unpack('n*', $data); 4541 } else { 4542 $this->seek($start); 4543 } 4544 4545 for ($glyph = 0; $glyph < $numberOfHMetrics; $glyph++) { 4546 4547 if (($numberOfHMetrics * 4) < $this->maxStrLenRead) { 4548 $aw = $arr[($glyph * 2) + 1]; 4549 } else { 4550 $aw = $this->read_ushort(); 4551 $lsb = $this->read_ushort(); 4552 } 4553 if (isset($glyphToChar[$glyph]) || $glyph == 0) { 4554 if ($aw >= (1 << 15)) { 4555 $aw = 0; 4556 } 4557 4558 // 1.03 Some (arabic) fonts have -ve values for width 4559 // although should be unsigned value - comes out as e.g. 65108 (intended -50) 4560 if ($glyph === 0) { 4561 $this->defaultWidth = $scale * $aw; 4562 continue; 4563 } 4564 4565 foreach ($glyphToChar[$glyph] as $char) { 4566 if ($char != 0 && $char != 65535) { 4567 $w = (int) round($scale * $aw); 4568 if ($w === 0) { 4569 $w = 65535; 4570 } 4571 if ($char < 196608) { 4572 $this->charWidths[$char * 2] = chr($w >> 8); 4573 $this->charWidths[$char * 2 + 1] = chr($w & 0xFF); 4574 $nCharWidths++; 4575 } 4576 } 4577 } 4578 } 4579 } 4580 4581 $data = $this->get_chunk(($start + $numberOfHMetrics * 4), ($numGlyphs * 2)); 4582 $arr = unpack("n*", $data); 4583 $diff = $numGlyphs - $numberOfHMetrics; 4584 $w = (int) round($scale * $aw); 4585 if ($w === 0) { 4586 $w = 65535; 4587 } 4588 for ($pos = 0; $pos < $diff; $pos++) { 4589 $glyph = $pos + $numberOfHMetrics; 4590 if (isset($glyphToChar[$glyph])) { 4591 foreach ($glyphToChar[$glyph] as $char) { 4592 if ($char != 0 && $char != 65535) { 4593 if ($char < 196608) { 4594 $this->charWidths[$char * 2] = chr($w >> 8); 4595 $this->charWidths[$char * 2 + 1] = chr($w & 0xFF); 4596 $nCharWidths++; 4597 } 4598 } 4599 } 4600 } 4601 } 4602 4603 // NB 65535 is a set width of 0 4604 // First bytes define number of chars in font 4605 $this->charWidths[0] = chr($nCharWidths >> 8); 4606 $this->charWidths[1] = chr($nCharWidths & 0xFF); 4607 } 4608 4609 function getHMetric($numberOfHMetrics, $gid) 4610 { 4611 $start = $this->seek_table("hmtx"); 4612 if ($gid < $numberOfHMetrics) { 4613 $this->seek($start + ($gid * 4)); 4614 $hm = fread($this->fh, 4); 4615 } else { 4616 $this->seek($start + (($numberOfHMetrics - 1) * 4)); 4617 $hm = fread($this->fh, 2); 4618 $this->seek($start + ($numberOfHMetrics * 2) + ($gid * 2)); 4619 $hm .= fread($this->fh, 2); 4620 } 4621 4622 return $hm; 4623 } 4624 4625 function getLOCA($indexToLocFormat, $numGlyphs) 4626 { 4627 $start = $this->seek_table('loca'); 4628 $this->glyphPos = []; 4629 if ($indexToLocFormat == 0) { 4630 $data = $this->get_chunk($start, ($numGlyphs * 2) + 2); 4631 $arr = unpack("n*", $data); 4632 for ($n = 0; $n <= $numGlyphs; $n++) { 4633 $this->glyphPos[] = ($arr[$n + 1] * 2); 4634 } 4635 } elseif ($indexToLocFormat == 1) { 4636 $data = $this->get_chunk($start, ($numGlyphs * 4) + 4); 4637 $arr = unpack("N*", $data); 4638 for ($n = 0; $n <= $numGlyphs; $n++) { 4639 $this->glyphPos[] = ($arr[$n + 1]); 4640 } 4641 } else { 4642 throw new \Mpdf\Exception\FontException('Unknown location table format ' . $indexToLocFormat); 4643 } 4644 } 4645 4646 /** 4647 * CMAP Format 4 4648 */ 4649 function getCMAP4($unicode_cmap_offset, &$glyphToChar, &$charToGlyph) 4650 { 4651 $this->maxUniChar = 0; 4652 $this->seek($unicode_cmap_offset + 2); 4653 $length = $this->read_ushort(); 4654 $limit = $unicode_cmap_offset + $length; 4655 $this->skip(2); 4656 4657 $segCount = $this->read_ushort() / 2; 4658 $this->skip(6); 4659 $endCount = []; 4660 4661 for ($i = 0; $i < $segCount; $i++) { 4662 $endCount[] = $this->read_ushort(); 4663 } 4664 4665 $this->skip(2); 4666 $startCount = []; 4667 4668 for ($i = 0; $i < $segCount; $i++) { 4669 $startCount[] = $this->read_ushort(); 4670 } 4671 4672 $idDelta = []; 4673 4674 for ($i = 0; $i < $segCount; $i++) { 4675 $idDelta[] = $this->read_short(); 4676 } // ???? was unsigned short 4677 4678 $idRangeOffset_start = $this->_pos; 4679 $idRangeOffset = []; 4680 4681 for ($i = 0; $i < $segCount; $i++) { 4682 $idRangeOffset[] = $this->read_ushort(); 4683 } 4684 4685 for ($n = 0; $n < $segCount; $n++) { 4686 $endpoint = ($endCount[$n] + 1); 4687 for ($unichar = $startCount[$n]; $unichar < $endpoint; $unichar++) { 4688 if ($idRangeOffset[$n] == 0) { 4689 $glyph = ($unichar + $idDelta[$n]) & 0xFFFF; 4690 } else { 4691 $offset = ($unichar - $startCount[$n]) * 2 + $idRangeOffset[$n]; 4692 $offset = $idRangeOffset_start + 2 * $n + $offset; 4693 if ($offset >= $limit) { 4694 $glyph = 0; 4695 } else { 4696 $glyph = $this->get_ushort($offset); 4697 if ($glyph != 0) { 4698 $glyph = ($glyph + $idDelta[$n]) & 0xFFFF; 4699 } 4700 } 4701 } 4702 $charToGlyph[$unichar] = $glyph; 4703 if ($unichar < 196608) { 4704 $this->maxUniChar = max($unichar, $this->maxUniChar); 4705 } 4706 $glyphToChar[$glyph][] = $unichar; 4707 } 4708 } 4709 } 4710 4711 function endTTFile(&$stm) 4712 { 4713 $stm = ''; 4714 $numTables = count($this->otables); 4715 $searchRange = 1; 4716 $entrySelector = 0; 4717 while ($searchRange * 2 <= $numTables) { 4718 $searchRange *= 2; 4719 $entrySelector += 1; 4720 } 4721 $searchRange *= 16; 4722 $rangeShift = $numTables * 16 - $searchRange; 4723 4724 // Header 4725 if (_TTF_MAC_HEADER) { 4726 $stm .= pack('Nnnnn', 0x74727565, $numTables, $searchRange, $entrySelector, $rangeShift); // Mac 4727 } else { 4728 $stm .= pack('Nnnnn', 0x00010000, $numTables, $searchRange, $entrySelector, $rangeShift); // Windows 4729 } 4730 4731 // Table directory 4732 $tables = $this->otables; 4733 ksort($tables); 4734 $offset = 12 + $numTables * 16; 4735 foreach ($tables as $tag => $data) { 4736 if ($tag === 'head') { 4737 $head_start = $offset; 4738 } 4739 $stm .= $tag; 4740 $checksum = $this->calcChecksum($data); 4741 $stm .= pack('nn', $checksum[0], $checksum[1]); 4742 $stm .= pack('NN', $offset, strlen($data)); 4743 $paddedLength = (strlen($data) + 3) & ~3; 4744 $offset += $paddedLength; 4745 } 4746 4747 // Table data 4748 foreach ($tables as $tag => $data) { 4749 $data .= "\0\0\0"; 4750 $stm .= substr($data, 0, (strlen($data) & ~3)); 4751 } 4752 4753 $checksum = $this->calcChecksum($stm); 4754 $checksum = $this->sub32([0xB1B0, 0xAFBA], $checksum); 4755 $chk = pack("nn", $checksum[0], $checksum[1]); 4756 $stm = $this->splice($stm, ($head_start + 8), $chk); 4757 4758 return $stm; 4759 } 4760 4761 function repackageTTF($file, $TTCfontID = 0, $debug = false, $useOTL = false) 4762 { 4763 $this->useOTL = $useOTL; 4764 $this->filename = $file; 4765 $this->fh = fopen($file, 'rb'); 4766 4767 if (!$this->fh) { 4768 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file "%s"', $file)); 4769 } 4770 4771 $this->_pos = 0; 4772 $this->charWidths = ''; 4773 $this->glyphPos = []; 4774 $this->charToGlyph = []; 4775 $this->tables = []; 4776 $this->otables = []; 4777 $this->ascent = 0; 4778 $this->descent = 0; 4779 $this->strikeoutSize = 0; 4780 $this->strikeoutPosition = 0; 4781 $this->numTTCFonts = 0; 4782 $this->TTCFonts = []; 4783 $this->skip(4); 4784 $this->maxUni = 0; 4785 4786 if ($TTCfontID > 0) { 4787 $this->version = $version = $this->read_ulong(); // TTC Header version now 4788 if (!in_array($version, [0x00010000, 0x00020000], true)) { 4789 throw new \Mpdf\Exception\FontException(sprintf('Error parsing TrueType Collection: version=%s - %s', $version, $file)); 4790 } 4791 $this->numTTCFonts = $this->read_ulong(); 4792 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 4793 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 4794 } 4795 $this->seek($this->TTCFonts[$TTCfontID]['offset']); 4796 $this->version = $version = $this->read_ulong(); // TTFont version again now 4797 } 4798 4799 $this->readTableDirectory($debug); 4800 $tags = ['OS/2', 'glyf', 'head', 'hhea', 'hmtx', 'loca', 'maxp', 'name', 'post', 'cvt ', 'fpgm', 'gasp', 'prep']; 4801 4802 foreach ($tags as $tag) { 4803 if (isset($this->tables[$tag])) { 4804 $this->add($tag, $this->get_table($tag)); 4805 } 4806 } 4807 4808 if ($useOTL) { 4809 4810 // maxp - Maximum profile table 4811 $this->seek_table('maxp'); 4812 $this->skip(4); 4813 $numGlyphs = $this->read_ushort(); 4814 4815 // cmap - Character to glyph index mapping table 4816 $cmap_offset = $this->seek_table('cmap'); 4817 $this->skip(2); 4818 $cmapTableCount = $this->read_ushort(); 4819 $unicode_cmap_offset = 0; 4820 for ($i = 0; $i < $cmapTableCount; $i++) { 4821 $platformID = $this->read_ushort(); 4822 $encodingID = $this->read_ushort(); 4823 $offset = $this->read_ulong(); 4824 $save_pos = $this->_pos; 4825 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode 4826 $format = $this->get_ushort($cmap_offset + $offset); 4827 if ($format == 4) { 4828 $unicode_cmap_offset = $cmap_offset + $offset; 4829 break; 4830 } 4831 } 4832 $this->seek($save_pos); 4833 } 4834 4835 if (!$unicode_cmap_offset) { 4836 throw new \Mpdf\Exception\FontException(sprintf('Font "%s" does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)', $this->filename)); 4837 } 4838 4839 $glyphToChar = []; 4840 $charToGlyph = []; 4841 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 4842 4843 // Map Unmapped glyphs - from $numGlyphs 4844 $bctr = 0xE000; 4845 for ($gid = 1; $gid < $numGlyphs; $gid++) { 4846 if (!isset($glyphToChar[$gid])) { 4847 while (isset($charToGlyph[$bctr])) { 4848 $bctr++; 4849 } // Avoid overwriting a glyph already mapped in PUA (6,400) 4850 if ($bctr > 0xF8FF) { 4851 throw new \Mpdf\Exception\FontException("Problem. Trying to repackage TF file; not enough space for unmapped glyphs"); 4852 } 4853 $glyphToChar[$gid][] = $bctr; 4854 $charToGlyph[$bctr] = $gid; 4855 $bctr++; 4856 } 4857 } 4858 4859 // Sort CID2GID map into segments of contiguous codes 4860 unset($charToGlyph[65535]); 4861 unset($charToGlyph[0]); 4862 4863 ksort($charToGlyph); 4864 $rangeid = 0; 4865 $range = []; 4866 $prevcid = -2; 4867 $prevglidx = -1; 4868 4869 // for each character 4870 foreach ($charToGlyph as $cid => $glidx) { 4871 if ($cid == ($prevcid + 1) && $glidx == ($prevglidx + 1)) { 4872 $range[$rangeid][] = $glidx; 4873 } else { 4874 // new range 4875 $rangeid = $cid; 4876 $range[$rangeid] = []; 4877 $range[$rangeid][] = $glidx; 4878 } 4879 $prevcid = $cid; 4880 $prevglidx = $glidx; 4881 } 4882 4883 // CMap table 4884 // cmap - Character to glyph mapping 4885 $segCount = count($range) + 1; // + 1 Last segment has missing character 0xFFFF 4886 $searchRange = 1; 4887 $entrySelector = 0; 4888 4889 while ($searchRange * 2 <= $segCount) { 4890 $searchRange *= 2; 4891 ++$entrySelector; 4892 } 4893 4894 $searchRange *= 2; 4895 $rangeShift = $segCount * 2 - $searchRange; 4896 $length = 16 + (8 * $segCount) + ($numGlyphs + 1); 4897 $cmap = [0, 3, // Index : version, number of encoding subtables 4898 0, 0, // Encoding Subtable : platform (UNI=0), encoding 0 4899 0, 28, // Encoding Subtable : offset (hi,lo) 4900 0, 3, // Encoding Subtable : platform (UNI=0), encoding 3 4901 0, 28, // Encoding Subtable : offset (hi,lo) 4902 3, 1, // Encoding Subtable : platform (MS=3), encoding 1 4903 0, 28, // Encoding Subtable : offset (hi,lo) 4904 4, $length, 0, // Format 4 Mapping subtable: format, length, language 4905 $segCount * 2, 4906 $searchRange, 4907 $entrySelector, 4908 $rangeShift]; 4909 4910 // endCode(s) 4911 foreach ($range as $start => $subrange) { 4912 $endCode = $start + (count($subrange) - 1); 4913 $cmap[] = $endCode; // endCode(s) 4914 } 4915 $cmap[] = 0xFFFF; // endCode of last Segment 4916 $cmap[] = 0; // reservedPad 4917 4918 // startCode(s) 4919 foreach ($range as $start => $subrange) { 4920 $cmap[] = $start; // startCode(s) 4921 } 4922 $cmap[] = 0xFFFF; // startCode of last Segment 4923 4924 // idDelta(s) 4925 foreach ($range as $start => $subrange) { 4926 $idDelta = -($start - $subrange[0]); 4927 $cmap[] = $idDelta; // idDelta(s) 4928 } 4929 4930 $cmap[] = 1; // idDelta of last Segment 4931 // idRangeOffset(s) 4932 foreach ($range as $subrange) { 4933 $cmap[] = 0; // idRangeOffset[segCount] Offset in bytes to glyph indexArray, or 0 4934 } 4935 4936 $cmap[] = 0; // idRangeOffset of last Segment 4937 foreach ($range as $subrange) { 4938 foreach ($subrange as $glidx) { 4939 $cmap[] = $glidx; 4940 } 4941 } 4942 $cmap[] = 0; // Mapping for last character 4943 $cmapstr = ''; 4944 foreach ($cmap as $cm) { 4945 $cmapstr .= pack('n', $cm); 4946 } 4947 4948 $this->add('cmap', $cmapstr); 4949 } else { 4950 $this->add('cmap', $this->get_table('cmap')); 4951 } 4952 4953 fclose($this->fh); 4954 $stm = ''; 4955 $this->endTTFile($stm); 4956 4957 return $stm; 4958 } 4959} 4960