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('Postscript outlines are not supported'); 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 return (fread($this->fh, $length)); 522 } 523 524 function get_table($tag) 525 { 526 list($pos, $length) = $this->get_table_pos($tag); 527 if ($length == 0) { 528 return ''; 529 } 530 fseek($this->fh, $pos); 531 532 return (fread($this->fh, $length)); 533 } 534 535 function add($tag, $data) 536 { 537 if ($tag === 'head') { 538 $data = $this->splice($data, 8, "\0\0\0\0"); 539 } 540 $this->otables[$tag] = $data; 541 } 542 543 function getCTG($file, $TTCfontID = 0, $debug = false, $useOTL = false) 544 { 545 // Only called if font is not to be used as embedded subset i.e. NOT called for SIP/SMP fonts 546 $this->useOTL = $useOTL; // mPDF 5.7.1 547 $this->filename = $file; 548 $this->fh = fopen($file, 'rb'); 549 550 if (!$this->fh) { 551 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file "%s"', $file)); 552 } 553 554 $this->_pos = 0; 555 $this->charWidths = ''; 556 $this->glyphPos = []; 557 $this->charToGlyph = []; 558 $this->tables = []; 559 $this->numTTCFonts = 0; 560 $this->TTCFonts = []; 561 $this->skip(4); 562 563 if ($TTCfontID > 0) { 564 $this->version = $version = $this->read_ulong(); // TTC Header version now 565 if (!in_array($version, [0x00010000, 0x00020000], true)) { 566 throw new \Mpdf\Exception\FontException(sprintf("Error parsing TrueType Collection: version=%s (%s)", $version, $file)); 567 } 568 $this->numTTCFonts = $this->read_ulong(); 569 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 570 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 571 } 572 $this->seek($this->TTCFonts[$TTCfontID]['offset']); 573 $this->version = $version = $this->read_ulong(); // TTFont version again now 574 } 575 $this->readTableDirectory($debug); 576 577 // cmap - Character to glyph index mapping table 578 $cmap_offset = $this->seek_table('cmap'); 579 $this->skip(2); 580 $cmapTableCount = $this->read_ushort(); 581 $unicode_cmap_offset = 0; 582 for ($i = 0; $i < $cmapTableCount; $i++) { 583 $platformID = $this->read_ushort(); 584 $encodingID = $this->read_ushort(); 585 $offset = $this->read_ulong(); 586 $save_pos = $this->_pos; 587 if ($platformID == 3 && $encodingID == 1) { // Microsoft, Unicode 588 $format = $this->get_ushort($cmap_offset + $offset); 589 if ($format == 4) { 590 $unicode_cmap_offset = $cmap_offset + $offset; 591 break; 592 } 593 } elseif ($platformID == 0) { // Unicode -- assume all encodings are compatible 594 $format = $this->get_ushort($cmap_offset + $offset); 595 if ($format == 4) { 596 $unicode_cmap_offset = $cmap_offset + $offset; 597 break; 598 } 599 } 600 $this->seek($save_pos); 601 } 602 603 $glyphToChar = []; 604 $charToGlyph = []; 605 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 606 607 // Map Unmapped glyphs - from $numGlyphs 608 if ($useOTL) { 609 $this->seek_table("maxp"); 610 $this->skip(4); 611 $numGlyphs = $this->read_ushort(); 612 $bctr = 0xE000; 613 for ($gid = 1; $gid < $numGlyphs; $gid++) { 614 if (!isset($glyphToChar[$gid])) { 615 while (isset($charToGlyph[$bctr])) { 616 $bctr++; 617 } // Avoid overwriting a glyph already mapped in PUA 618 if ($bctr > 0xF8FF) { 619 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)); 620 } 621 $glyphToChar[$gid][] = $bctr; 622 $charToGlyph[$bctr] = $gid; 623 $bctr++; 624 } 625 } 626 } 627 628 fclose($this->fh); 629 630 return $charToGlyph; 631 } 632 633 function getTTCFonts($file) 634 { 635 $this->filename = $file; 636 637 $this->fh = fopen($file, 'rb'); 638 if (!$this->fh) { 639 throw new \Mpdf\Exception\FontException(sprintf('Unable to open file "%s"', $file)); 640 } 641 642 $this->numTTCFonts = 0; 643 $this->TTCFonts = []; 644 $this->version = $version = $this->read_ulong(); 645 if ($version === 0x74746366) { 646 $this->version = $version = $this->read_ulong(); // TTC Header version now 647 if (!in_array($version, [0x00010000, 0x00020000], true)) { 648 throw new \Mpdf\Exception\FontException(sprintf("Error parsing TrueType Collection: version=%s (%s)", $version, $file)); 649 } 650 } else { 651 throw new \Mpdf\Exception\FontException(sprintf("Not a TrueType Collection: version=%s (%s)", $version, $file)); 652 } 653 654 $this->numTTCFonts = $this->read_ulong(); 655 for ($i = 1; $i <= $this->numTTCFonts; $i++) { 656 $this->TTCFonts[$i]['offset'] = $this->read_ulong(); 657 } 658 } 659 660 function extractInfo($debug = false, $BMPonly = false, $useOTL = 0) 661 { 662 // Values are all set to 0 or blank at start of getMetrics 663 // name - Naming table 664 $name_offset = $this->seek_table("name"); 665 $format = $this->read_ushort(); 666 if ($format != 0 && $format != 1) { 667 throw new \Mpdf\Exception\FontException("Error loading font: Unknown name table format $format for font $this->filename"); 668 } 669 670 $numRecords = $this->read_ushort(); 671 $string_data_offset = $name_offset + $this->read_ushort(); 672 $names = [1 => '', 2 => '', 3 => '', 4 => '', 6 => '']; 673 $K = array_keys($names); 674 $nameCount = count($names); 675 676 for ($i = 0; $i < $numRecords; $i++) { 677 678 $platformId = $this->read_ushort(); 679 $encodingId = $this->read_ushort(); 680 $languageId = $this->read_ushort(); 681 $nameId = $this->read_ushort(); 682 $length = $this->read_ushort(); 683 $offset = $this->read_ushort(); 684 685 if (!in_array($nameId, $K)) { 686 continue; 687 } 688 689 $N = ''; 690 if ($platformId == 3 && $encodingId == 1 && $languageId == 0x409) { // Microsoft, Unicode, US English, PS Name 691 $opos = $this->_pos; 692 $this->seek($string_data_offset + $offset); 693 if ($length % 2 != 0) { 694 throw new \Mpdf\Exception\FontException("Error loading font: PostScript name is UTF-16BE string of odd length for font $this->filename"); 695 } 696 $length /= 2; 697 $N = ''; 698 while ($length > 0) { 699 $char = $this->read_ushort(); 700 $N .= (chr($char)); 701 $length -= 1; 702 } 703 $this->_pos = $opos; 704 $this->seek($opos); 705 } elseif ($platformId == 1 && $encodingId == 0 && $languageId == 0) { // Macintosh, Roman, English, PS Name 706 $opos = $this->_pos; 707 $N = $this->get_chunk($string_data_offset + $offset, $length); 708 $this->_pos = $opos; 709 $this->seek($opos); 710 } 711 if ($N && $names[$nameId] == '') { 712 $names[$nameId] = $N; 713 $nameCount -= 1; 714 if ($nameCount == 0) { 715 break; 716 } 717 } 718 } 719 720 if ($names[6]) { 721 $psName = $names[6]; 722 } elseif ($names[4]) { 723 $psName = preg_replace('/ /', '-', $names[4]); 724 } elseif ($names[1]) { 725 $psName = preg_replace('/ /', '-', $names[1]); 726 } else { 727 $psName = ''; 728 } 729 730 if (!$psName) { 731 throw new \Mpdf\Exception\FontException("Error loading font: Could not find PostScript font name '$this->filename'"); 732 } 733 734 // CHECK IF psName valid (PadaukBook contains illegal characters in Name ID 6 i.e. Postscript Name) 735 $psNameInvalid = false; 736 $nameLength = strlen($psName); 737 for ($i = 0; $i < $nameLength; $i++) { 738 $c = $psName[$i]; 739 $oc = ord($c); 740 if ($oc > 126 || strpos(' [](){}<>/%', $c) !== false) { 741 //throw new \Mpdf\Exception\FontException("psName=".$psName." contains invalid character ".$c." ie U+".ord(c)); 742 $psNameInvalid = true; 743 break; 744 } 745 } 746 747 if ($psNameInvalid && $names[4]) { 748 $psName = preg_replace('/ /', '-', $names[4]); 749 } 750 751 $this->name = $psName; 752 if ($names[1]) { 753 $this->familyName = $names[1]; 754 } else { 755 $this->familyName = $psName; 756 } 757 if ($names[2]) { 758 $this->styleName = $names[2]; 759 } else { 760 $this->styleName = 'Regular'; 761 } 762 if ($names[4]) { 763 $this->fullName = $names[4]; 764 } else { 765 $this->fullName = $psName; 766 } 767 if ($names[3]) { 768 $this->uniqueFontID = $names[3]; 769 } else { 770 $this->uniqueFontID = $psName; 771 } 772 773 if (!$psNameInvalid && $names[6]) { 774 $this->fullName = $names[6]; 775 } 776 777 // head - Font header table 778 $this->seek_table('head'); 779 if ($debug) { 780 $ver_maj = $this->read_ushort(); 781 $ver_min = $this->read_ushort(); 782 if ($ver_maj != 1) { 783 throw new \Mpdf\Exception\FontException('Error loading font: Unknown head table version ' . $ver_maj . '.' . $ver_min); 784 } 785 $this->fontRevision = $this->read_ushort() . $this->read_ushort(); 786 787 $this->skip(4); 788 $magic = $this->read_ulong(); 789 if ($magic !== 0x5F0F3CF5) { 790 throw new \Mpdf\Exception\FontException('Error loading font: Invalid head table magic ' . $magic); 791 } 792 $this->skip(2); 793 } else { 794 $this->skip(18); 795 } 796 $this->unitsPerEm = $unitsPerEm = $this->read_ushort(); 797 $scale = 1000 / $unitsPerEm; 798 $this->skip(16); 799 $xMin = $this->read_short(); 800 $yMin = $this->read_short(); 801 $xMax = $this->read_short(); 802 $yMax = $this->read_short(); 803 $this->bbox = [($xMin * $scale), ($yMin * $scale), ($xMax * $scale), ($yMax * $scale)]; 804 805 $this->skip(3 * 2); 806 $indexToLocFormat = $this->read_ushort(); 807 $glyphDataFormat = $this->read_ushort(); 808 if ($glyphDataFormat != 0) { 809 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown glyph data format %s', $glyphDataFormat)); 810 } 811 812 // hhea metrics table 813 if (isset($this->tables["hhea"])) { 814 $this->seek_table("hhea"); 815 $this->skip(4); 816 $hheaAscender = $this->read_short(); 817 $hheaDescender = $this->read_short(); 818 $hheaLineGap = $this->read_short(); 819 $hheaAdvanceWidthMax = $this->read_ushort(); 820 $this->hheaascent = ($hheaAscender * $scale); 821 $this->hheadescent = ($hheaDescender * $scale); 822 $this->hhealineGap = ($hheaLineGap * $scale); 823 $this->advanceWidthMax = ($hheaAdvanceWidthMax * $scale); 824 } 825 826 // OS/2 - OS/2 and Windows metrics table 827 $use_typo_metrics = false; 828 if (isset($this->tables["OS/2"])) { 829 $this->seek_table("OS/2"); 830 $version = $this->read_ushort(); 831 $this->skip(2); 832 $usWeightClass = $this->read_ushort(); 833 $this->skip(2); 834 $fsType = $this->read_ushort(); 835 if ($fsType == 0x0002 || ($fsType & 0x0300) != 0) { 836 $this->restrictedUse = true; 837 } 838 839 $this->skip(16); 840 $yStrikeoutSize = $this->read_short(); 841 $yStrikeoutPosition = $this->read_short(); 842 $this->strikeoutSize = ($yStrikeoutSize * $scale); 843 $this->strikeoutPosition = ($yStrikeoutPosition * $scale); 844 845 $sF = $this->read_short(); 846 $this->sFamilyClass = ($sF >> 8); 847 $this->sFamilySubClass = ($sF & 0xFF); 848 $this->_pos += 10; //PANOSE = 10 byte length 849 $panose = fread($this->fh, 10); 850 $this->panose = []; 851 $panoseLenght = strlen($panose); 852 for ($p = 0; $p < $panoseLenght; $p++) { 853 $this->panose[] = ord($panose[$p]); 854 } 855 856 $this->skip(20); 857 $fsSelection = $this->read_ushort(); 858 $use_typo_metrics = (($fsSelection & 0x80) === 0x80); // bit#7 = USE_TYPO_METRICS 859 $this->skip(4); 860 861 $sTypoAscender = $this->read_short(); 862 $sTypoDescender = $this->read_short(); 863 $sTypoLineGap = $this->read_short(); 864 865 if ($sTypoAscender) { 866 $this->typoAscender = ($sTypoAscender * $scale); 867 } 868 if ($sTypoDescender) { 869 $this->typoDescender = ($sTypoDescender * $scale); 870 } 871 if ($sTypoLineGap) { 872 $this->typoLineGap = ($sTypoLineGap * $scale); 873 } 874 875 $usWinAscent = $this->read_ushort(); 876 $usWinDescent = $this->read_ushort(); 877 if ($usWinAscent) { 878 $this->usWinAscent = ($usWinAscent * $scale); 879 } 880 if ($usWinDescent) { 881 $this->usWinDescent = ($usWinDescent * $scale); 882 } 883 884 if ($version > 1) { 885 $this->skip(8); 886 $sxHeight = $this->read_short(); 887 $this->xHeight = ($sxHeight * $scale); 888 $sCapHeight = $this->read_short(); 889 $this->capHeight = ($sCapHeight * $scale); 890 } 891 } else { 892 $usWeightClass = 400; 893 } 894 $this->stemV = 50 + (int) (($usWeightClass / 65.0) ** 2); 895 896 // FONT DESCRIPTOR METRICS 897 if ($this->fontDescriptor === 'winTypo') { 898 $this->ascent = $this->typoAscender; 899 $this->descent = $this->typoDescender; 900 $this->lineGap = $this->typoLineGap; 901 } elseif ($this->fontDescriptor === 'mac') { 902 $this->ascent = $this->hheaascent; 903 $this->descent = $this->hheadescent; 904 $this->lineGap = $this->hhealineGap; 905 } else { // $this->fontDescriptor === 'win' 906 $this->ascent = $this->usWinAscent; 907 $this->descent = -$this->usWinDescent; 908 $this->lineGap = 0; 909 910 // Special case - if either the winAscent or winDescent are greater than the 911 // font bounding box yMin yMax, then reduce them accordingly. 912 // This works with Myanmar Text (Windows 8 version) to give a 913 // line-height normal that is equivalent to that produced in browsers. 914 // Also Khmer OS = compatible with MSWord, Wordpad and browser. 915 if ($this->ascent > $this->bbox[3]) { 916 $this->ascent = $this->bbox[3]; 917 } 918 919 if ($this->descent < $this->bbox[1]) { 920 $this->descent = $this->bbox[1]; 921 } 922 923 // Override case - if the USE_TYPO_METRICS bit is set on OS/2 fsSelection 924 // this is telling the font to use the sTypo values and not the usWinAscent values. 925 // This works as a fix with Cambria Math to give a normal line-height; 926 // at present, this is the only font I have found with this bit set; 927 // although note that MS WordPad and windows FF browser uses the big line-height from winAscent 928 // but Word 2007 get it right 929 if ($use_typo_metrics && $this->typoAscender) { 930 $this->ascent = $this->typoAscender; 931 $this->descent = $this->typoDescender; 932 $this->lineGap = $this->typoLineGap; 933 } 934 } 935 936 // post - PostScript table 937 $this->seek_table('post'); 938 if ($debug) { 939 $ver_maj = $this->read_ushort(); 940 if ($ver_maj < 1 || $ver_maj > 4) { 941 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown post table version %s', $ver_maj)); 942 } 943 } else { 944 $this->skip(4); 945 } 946 947 $this->italicAngle = $this->read_short() + $this->read_ushort() / 65536.0; 948 $this->underlinePosition = $this->read_short() * $scale; 949 $this->underlineThickness = $this->read_short() * $scale; 950 $isFixedPitch = $this->read_ulong(); 951 952 $this->flags = 4; 953 954 if ($this->italicAngle != 0) { 955 $this->flags |= 64; 956 } 957 if ($usWeightClass >= 600) { 958 $this->flags |= 262144; 959 } 960 if ($isFixedPitch) { 961 $this->flags |= 1; 962 } 963 964 // hhea - Horizontal header table 965 $this->seek_table('hhea'); 966 if ($debug) { 967 $ver_maj = $this->read_ushort(); 968 if ($ver_maj != 1) { 969 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown hhea table version %s', $ver_maj)); 970 } 971 $this->skip(28); 972 } else { 973 $this->skip(32); 974 } 975 976 $metricDataFormat = $this->read_ushort(); 977 978 if ($metricDataFormat != 0) { 979 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown horizontal metric data format "%s"', $metricDataFormat)); 980 } 981 982 $numberOfHMetrics = $this->read_ushort(); 983 984 if ($numberOfHMetrics == 0) { 985 throw new \Mpdf\Exception\FontException('Error loading font: Number of horizontal metrics is 0'); 986 } 987 988 // maxp - Maximum profile table 989 $this->seek_table('maxp'); 990 if ($debug) { 991 $ver_maj = $this->read_ushort(); 992 if ($ver_maj != 1) { 993 throw new \Mpdf\Exception\FontException(sprintf('Error loading font: Unknown maxp table version %s', $ver_maj)); 994 } 995 } else { 996 $this->skip(4); 997 } 998 $numGlyphs = $this->read_ushort(); 999 1000 // cmap - Character to glyph index mapping table 1001 $cmap_offset = $this->seek_table('cmap'); 1002 $this->skip(2); 1003 $cmapTableCount = $this->read_ushort(); 1004 $unicode_cmap_offset = 0; 1005 for ($i = 0; $i < $cmapTableCount; $i++) { 1006 $platformID = $this->read_ushort(); 1007 $encodingID = $this->read_ushort(); 1008 $offset = $this->read_ulong(); 1009 $save_pos = $this->_pos; 1010 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode 1011 $format = $this->get_ushort($cmap_offset + $offset); 1012 if ($format == 4) { 1013 if (!$unicode_cmap_offset) { 1014 $unicode_cmap_offset = $cmap_offset + $offset; 1015 } 1016 if ($BMPonly) { 1017 break; 1018 } 1019 } 1020 } elseif ((($platformID == 3 && $encodingID == 10) || $platformID == 0) && !$BMPonly) { // Microsoft, Unicode Format 12 table HKCS 1021 $format = $this->get_ushort($cmap_offset + $offset); 1022 if ($format == 12) { 1023 $unicode_cmap_offset = $cmap_offset + $offset; 1024 break; 1025 } 1026 } 1027 $this->seek($save_pos); 1028 } 1029 1030 if (!$unicode_cmap_offset) { 1031 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)); 1032 } 1033 1034 $sipset = false; 1035 $smpset = false; 1036 1037 $this->rtlPUAstr = ''; 1038 $this->GSUBScriptLang = []; 1039 $this->GSUBFeatures = []; 1040 $this->GSUBLookups = []; 1041 $this->GPOSScriptLang = []; 1042 $this->GPOSFeatures = []; 1043 $this->GPOSLookups = []; 1044 $this->glyphIDtoUni = ''; 1045 1046 // Format 12 CMAP does characters above Unicode BMP i.e. some HKCS characters U+20000 and above 1047 if ($format == 12 && !$BMPonly) { 1048 $this->maxUniChar = 0; 1049 $this->seek($unicode_cmap_offset + 4); 1050 $length = $this->read_ulong(); 1051 $limit = $unicode_cmap_offset + $length; 1052 $this->skip(4); 1053 1054 $nGroups = $this->read_ulong(); 1055 1056 $glyphToChar = []; 1057 $charToGlyph = []; 1058 for ($i = 0; $i < $nGroups; $i++) { 1059 $startCharCode = $this->read_ulong(); 1060 $endCharCode = $this->read_ulong(); 1061 $startGlyphCode = $this->read_ulong(); 1062 // ZZZ98 1063 if ($endCharCode > 0x20000 && $endCharCode < 0x2FFFF) { 1064 $sipset = true; 1065 } elseif ($endCharCode > 0x10000 && $endCharCode < 0x1FFFF) { 1066 $smpset = true; 1067 } 1068 $offset = 0; 1069 for ($unichar = $startCharCode; $unichar <= $endCharCode; $unichar++) { 1070 $glyph = $startGlyphCode + $offset; 1071 $offset++; 1072 // ZZZ98 1073 if ($unichar < 0x30000) { 1074 $charToGlyph[$unichar] = $glyph; 1075 $this->maxUniChar = max($unichar, $this->maxUniChar); 1076 $glyphToChar[$glyph][] = $unichar; 1077 } 1078 } 1079 } 1080 } else { 1081 $glyphToChar = []; 1082 $charToGlyph = []; 1083 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph); 1084 } 1085 $this->sipset = $sipset; 1086 $this->smpset = $smpset; 1087 1088 // Map Unmapped glyphs (or glyphs mapped to upper PUA U+F00000 onwards i.e. > U+2FFFF) - from $numGlyphs 1089 if ($this->useOTL) { 1090 1091 $bctr = 0xE000; 1092 1093 for ($gid = 1; $gid < $numGlyphs; $gid++) { 1094 1095 if (!isset($glyphToChar[$gid])) { 1096 1097 while (isset($charToGlyph[$bctr])) { 1098 $bctr++; 1099 } 1100 1101 // Avoid overwriting a glyph already mapped in PUA 1102 // ZZZ98 1103 if (($bctr > 0xF8FF) && ($bctr < 0x2CEB0)) { 1104 if (!$BMPonly) { 1105 $bctr = 0x2CEB0; // Use unassigned area 0x2CEB0 to 0x2F7FF (space for 10,000 characters) 1106 $this->sipset = $sipset = true; // forces subsetting; also ensure charwidths are saved 1107 while (isset($charToGlyph[$bctr])) { 1108 $bctr++; 1109 } 1110 } else { 1111 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])); 1112 } 1113 } 1114 1115 $glyphToChar[$gid][] = $bctr; 1116 $charToGlyph[$bctr] = $gid; 1117 $this->maxUniChar = max($bctr, $this->maxUniChar); 1118 $bctr++; 1119 } 1120 } 1121 } 1122 1123 $this->glyphToChar = $glyphToChar; 1124 1125 $this->GSUBScriptLang = []; 1126 $this->rtlPUAstr = ''; 1127 if ($useOTL) { 1128 $this->_getGDEFtables(); 1129 list($this->GSUBScriptLang, $this->GSUBFeatures, $this->GSUBLookups, $this->rtlPUAstr) = $this->_getGSUBtables(); 1130 list($this->GPOSScriptLang, $this->GPOSFeatures, $this->GPOSLookups) = $this->_getGPOStables(); 1131 $this->glyphIDtoUni = str_pad('', 256 * 256 * 3, "\x00"); 1132 foreach ($glyphToChar as $gid => $arr) { 1133 if (isset($glyphToChar[$gid][0])) { 1134 $char = $glyphToChar[$gid][0]; 1135 1136 if ($char != 0 && $char != 65535) { 1137 $this->glyphIDtoUni[$gid * 3] = chr($char >> 16); 1138 $this->glyphIDtoUni[$gid * 3 + 1] = chr(($char >> 8) & 0xFF); 1139 $this->glyphIDtoUni[$gid * 3 + 2] = chr($char & 0xFF); 1140 } 1141 } 1142 } 1143 } 1144 1145 // if xHeight and/or CapHeight are not available from OS/2 (e.g. eraly versions) 1146 // Calculate from yMax of 'x' or 'H' Glyphs... 1147 if ($this->xHeight == 0) { 1148 if (isset($charToGlyph[0x78])) { 1149 $gidx = $charToGlyph[0x78]; // U+0078 (LATIN SMALL LETTER X) 1150 $start = $this->seek_table('loca'); 1151 if ($indexToLocFormat == 0) { 1152 $this->skip($gidx * 2); 1153 $locax = $this->read_ushort() * 2; 1154 } elseif ($indexToLocFormat == 1) { 1155 $this->skip($gidx * 4); 1156 $locax = $this->read_ulong(); 1157 } 1158 $start = $this->seek_table('glyf'); 1159 $this->skip($locax); 1160 $this->skip(8); 1161 $yMaxx = $this->read_short(); 1162 $this->xHeight = $yMaxx * $scale; 1163 } 1164 } 1165 1166 if ($this->capHeight == 0) { 1167 if (isset($charToGlyph[0x48])) { 1168 $gidH = $charToGlyph[0x48]; // U+0048 (LATIN CAPITAL LETTER H) 1169 $start = $this->seek_table('loca'); 1170 if ($indexToLocFormat == 0) { 1171 $this->skip($gidH * 2); 1172 $locaH = $this->read_ushort() * 2; 1173 } elseif ($indexToLocFormat == 1) { 1174 $this->skip($gidH * 4); 1175 $locaH = $this->read_ulong(); 1176 } 1177 $start = $this->seek_table('glyf'); 1178 $this->skip($locaH); 1179 $this->skip(8); 1180 $yMaxH = $this->read_short(); 1181 $this->capHeight = $yMaxH * $scale; 1182 } else { 1183 $this->capHeight = $this->ascent; 1184 } 1185 // final default is to set it = to Ascent 1186 } 1187 1188 // hmtx - Horizontal metrics table 1189 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale); 1190 1191 // kern - Kerning pair table 1192 // Recognises old form of Kerning table - as required by Windows - Format 0 only 1193 $kern_offset = $this->seek_table("kern"); 1194 $version = $this->read_ushort(); 1195 $nTables = $this->read_ushort(); 1196 1197 // subtable header 1198 $sversion = $this->read_ushort(); 1199 $slength = $this->read_ushort(); 1200 $scoverage = $this->read_ushort(); 1201 $format = $scoverage >> 8; 1202 if ($kern_offset && $version == 0 && $format == 0) { 1203 // Format 0 1204 $nPairs = $this->read_ushort(); 1205 $this->skip(6); 1206 for ($i = 0; $i < $nPairs; $i++) { 1207 $left = $this->read_ushort(); 1208 $right = $this->read_ushort(); 1209 $val = $this->read_short(); 1210 if (isset($glyphToChar[$left]) && count($glyphToChar[$left]) == 1 && isset($glyphToChar[$right]) && count($glyphToChar[$right]) == 1) { 1211 if ($left != 32 && $right != 32) { 1212 $this->kerninfo[$glyphToChar[$left][0]][$glyphToChar[$right][0]] = intval($val * $scale); 1213 } 1214 } 1215 } 1216 } 1217 } 1218 1219 function _getGDEFtables() 1220 { 1221 // http://www.microsoft.com/typography/otspec/gdef.htm 1222 if (isset($this->tables["GDEF"])) { 1223 $gdef_offset = $this->seek_table("GDEF"); 1224 1225 // ULONG Version of the GDEF table-currently 0x00010000 1226 $ver_maj = $this->read_ushort(); 1227 $ver_min = $this->read_ushort(); 1228 $GlyphClassDef_offset = $this->read_ushort(); 1229 $AttachList_offset = $this->read_ushort(); 1230 $LigCaretList_offset = $this->read_ushort(); 1231 $MarkAttachClassDef_offset = $this->read_ushort(); 1232 1233 // Version 0x00010002 of GDEF header contains additional Offset to a list defining mark glyph set definitions (MarkGlyphSetDef) 1234 if ($ver_min == 2) { 1235 $MarkGlyphSetsDef_offset = $this->read_ushort(); 1236 } 1237 1238 // GlyphClassDef 1239 if ($GlyphClassDef_offset) { 1240 1241 $this->seek($gdef_offset + $GlyphClassDef_offset); 1242 // 1 Base glyph (single character, spacing glyph) 1243 // 2 Ligature glyph (multiple character, spacing glyph) 1244 // 3 Mark glyph (non-spacing combining glyph) 1245 // 4 Component glyph (part of single character, spacing glyph) 1246 $GlyphByClass = $this->_getClassDefinitionTable(); 1247 } else { 1248 $GlyphByClass = []; 1249 } 1250 1251 if (isset($GlyphByClass[1]) && count($GlyphByClass[1]) > 0) { 1252 $this->GlyphClassBases = ' ' . implode('| ', $GlyphByClass[1]); 1253 } else { 1254 $this->GlyphClassBases = ''; 1255 } 1256 if (isset($GlyphByClass[2]) && count($GlyphByClass[2]) > 0) { 1257 $this->GlyphClassLigatures = ' ' . implode('| ', $GlyphByClass[2]); 1258 } else { 1259 $this->GlyphClassLigatures = ''; 1260 } 1261 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) { 1262 $this->GlyphClassMarks = ' ' . implode('| ', $GlyphByClass[3]); 1263 } else { 1264 $this->GlyphClassMarks = ''; 1265 } 1266 if (isset($GlyphByClass[4]) && count($GlyphByClass[4]) > 0) { 1267 $this->GlyphClassComponents = ' ' . implode('| ', $GlyphByClass[4]); 1268 } else { 1269 $this->GlyphClassComponents = ''; 1270 } 1271 1272 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) { 1273 $Marks = $GlyphByClass[3]; 1274 } else { // to use for MarkAttachmentType 1275 $Marks = []; 1276 } 1277 1278 /* Required for GPOS 1279 // Attachment List 1280 if ($AttachList_offset) { 1281 $this->seek($gdef_offset+$AttachList_offset ); 1282 } 1283 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. 1284 1285 The Attachment List table (AttachList) may be used to cache attachment point coordinates along with glyph bitmaps. 1286 1287 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. 1288 AttachList table 1289 Type Name Description 1290 Offset Coverage Offset to Coverage table - from beginning of AttachList table 1291 uint16 GlyphCount Number of glyphs with attachment points 1292 Offset AttachPoint[GlyphCount] Array of offsets to AttachPoint tables-from beginning of AttachList table-in Coverage Index order 1293 1294 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. 1295 1296 AttachPoint table 1297 Type Name Description 1298 uint16 PointCount Number of attachment points on this glyph 1299 uint16 PointIndex[PointCount] Array of contour point indices -in increasing numerical order 1300 1301 See Example 3 - http://www.microsoft.com/typography/otspec/gdef.htm 1302 */ 1303 1304 // Ligature Caret List 1305 // The Ligature Caret List table (LigCaretList) defines caret positions for all the ligatures in a font. 1306 // Not required for mDPF 1307 // MarkAttachmentType 1308 if ($MarkAttachClassDef_offset) { 1309 $this->seek($gdef_offset + $MarkAttachClassDef_offset); 1310 $MarkAttachmentTypes = $this->_getClassDefinitionTable(); 1311 foreach ($MarkAttachmentTypes as $class => $glyphs) { 1312 if (is_array($Marks) && count($Marks)) { 1313 $mat = array_diff($Marks, $MarkAttachmentTypes[$class]); 1314 sort($mat, SORT_STRING); 1315 } else { 1316 $mat = []; 1317 } 1318 1319 $this->MarkAttachmentType[$class] = ' ' . implode('| ', $mat); 1320 } 1321 } else { 1322 $this->MarkAttachmentType = []; 1323 } 1324 1325 // MarkGlyphSets only in Version 0x00010002 of GDEF 1326 if ($ver_min == 2 && $MarkGlyphSetsDef_offset) { 1327 $this->seek($gdef_offset + $MarkGlyphSetsDef_offset); 1328 $MarkSetTableFormat = $this->read_ushort(); 1329 $MarkSetCount = $this->read_ushort(); 1330 $MarkSetOffset = []; 1331 for ($i = 0; $i < $MarkSetCount; $i++) { 1332 $MarkSetOffset[] = $this->read_ulong(); 1333 } 1334 for ($i = 0; $i < $MarkSetCount; $i++) { 1335 $this->seek($MarkSetOffset[$i]); 1336 $glyphs = $this->_getCoverage(); 1337 $this->MarkGlyphSets[$i] = ' ' . implode('| ', $glyphs); 1338 } 1339 } else { 1340 $this->MarkGlyphSets = []; 1341 } 1342 } else { 1343 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)); 1344 } 1345 1346 $GSUB_offset = 0; 1347 $GPOS_offset = 0; 1348 $GSUB_length = 0; 1349 1350 $s = ''; 1351 1352 if (isset($this->tables['GSUB'])) { 1353 $GSUB_offset = $this->seek_table('GSUB'); 1354 $GSUB_length = $this->tables['GSUB']['length']; 1355 $s .= fread($this->fh, $this->tables['GSUB']['length']); 1356 } 1357 1358 if (isset($this->tables['GPOS'])) { 1359 $GPOS_offset = $this->seek_table('GPOS'); 1360 $s .= fread($this->fh, $this->tables['GPOS']['length']); 1361 } 1362 1363 if ($s) { 1364 $this->fontCache->write($this->fontkey . '.GSUBGPOStables.dat', $s); 1365 } 1366 1367 $font = [ 1368 'GSUB_offset' => $GSUB_offset, 1369 'GPOS_offset' => $GPOS_offset, 1370 'GSUB_length' => $GSUB_length, 1371 'GlyphClassBases' => $this->GlyphClassBases, 1372 'GlyphClassMarks' => $this->GlyphClassMarks, 1373 'GlyphClassLigatures' => $this->GlyphClassLigatures, 1374 'GlyphClassComponents' => $this->GlyphClassComponents, 1375 'MarkGlyphSets' => $this->MarkGlyphSets, 1376 'MarkAttachmentType' => $this->MarkAttachmentType, 1377 ]; 1378 1379 $this->fontCache->jsonWrite($this->fontkey . '.GDEFdata.json', $font); 1380 } 1381 1382 function _getClassDefinitionTable() 1383 { 1384 // NB Any glyph not included in the range of covered GlyphIDs automatically belongs to Class 0. This is not returned by this function 1385 $ClassFormat = $this->read_ushort(); 1386 $GlyphByClass = []; 1387 1388 if ($ClassFormat == 1) { 1389 $StartGlyph = $this->read_ushort(); 1390 $GlyphCount = $this->read_ushort(); 1391 for ($i = 0; $i < $GlyphCount; $i++) { 1392 $gid = $StartGlyph + $i; 1393 $class = $this->read_ushort(); 1394 // Several fonts (mainly dejavu.../Freeserif etc) have a MarkAttachClassDef Format 1, where StartGlyph is 0 and GlyphCount is 1 1395 // This doesn't seem to do anything useful? 1396 // Freeserif does not have $this->glyphToChar[0] allocated and would throw an error, so check if isset: 1397 if (isset($this->glyphToChar[$gid][0])) { 1398 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]); 1399 } 1400 } 1401 } elseif ($ClassFormat == 2) { 1402 $tableCount = $this->read_ushort(); 1403 for ($i = 0; $i < $tableCount; $i++) { 1404 $startGlyphID = $this->read_ushort(); 1405 $endGlyphID = $this->read_ushort(); 1406 $class = $this->read_ushort(); 1407 for ($gid = $startGlyphID; $gid <= $endGlyphID; $gid++) { 1408 if (isset($this->glyphToChar[$gid][0])) { 1409 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]); 1410 } 1411 } 1412 } 1413 } 1414 foreach ($GlyphByClass as $class => $glyphs) { 1415 sort($GlyphByClass[$class], SORT_STRING); // SORT makes it easier to read in development ? order not important ??? 1416 } 1417 ksort($GlyphByClass); 1418 1419 return $GlyphByClass; 1420 } 1421 1422 /** 1423 * GSUB - Glyph Substitution 1424 */ 1425 function _getGSUBtables() 1426 { 1427 if (!isset($this->tables['GSUB'])) { 1428 return [[], [], [], '']; 1429 } 1430 1431 $ffeats = []; 1432 $gsub_offset = $this->seek_table('GSUB'); 1433 $this->skip(4); 1434 $ScriptList_offset = $gsub_offset + $this->read_ushort(); 1435 $FeatureList_offset = $gsub_offset + $this->read_ushort(); 1436 $LookupList_offset = $gsub_offset + $this->read_ushort(); 1437 1438 // ScriptList 1439 $this->seek($ScriptList_offset); 1440 $ScriptCount = $this->read_ushort(); 1441 for ($i = 0; $i < $ScriptCount; $i++) { 1442 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc. 1443 $ScriptTableOffset = $this->read_ushort(); 1444 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset; 1445 } 1446 1447 // Script Table 1448 foreach ($ffeats as $t => $o) { 1449 $ls = []; 1450 $this->seek($o); 1451 $DefLangSys_offset = $this->read_ushort(); 1452 if ($DefLangSys_offset > 0) { 1453 $ls['DFLT'] = $DefLangSys_offset + $o; 1454 } 1455 $LangSysCount = $this->read_ushort(); 1456 for ($i = 0; $i < $LangSysCount; $i++) { 1457 $LangTag = $this->read_tag(); // = 1458 $LangTableOffset = $this->read_ushort(); 1459 $ls[$LangTag] = $o + $LangTableOffset; 1460 } 1461 $ffeats[$t] = $ls; 1462 } 1463 1464 // Get FeatureIndexList 1465 // LangSys Table - from first listed langsys 1466 foreach ($ffeats as $st => $scripts) { 1467 foreach ($scripts as $t => $o) { 1468 $FeatureIndex = []; 1469 $langsystable_offset = $o; 1470 $this->seek($langsystable_offset); 1471 $LookUpOrder = $this->read_ushort(); //==NULL 1472 $ReqFeatureIndex = $this->read_ushort(); 1473 if ($ReqFeatureIndex != 0xFFFF) { 1474 $FeatureIndex[] = $ReqFeatureIndex; 1475 } 1476 $FeatureCount = $this->read_ushort(); 1477 for ($i = 0; $i < $FeatureCount; $i++) { 1478 $FeatureIndex[] = $this->read_ushort(); // = index of feature 1479 } 1480 $ffeats[$st][$t] = $FeatureIndex; 1481 } 1482 } 1483 1484 // Feauture List => LookupListIndex es 1485 $this->seek($FeatureList_offset); 1486 $FeatureCount = $this->read_ushort(); 1487 $Feature = []; 1488 1489 for ($i = 0; $i < $FeatureCount; $i++) { 1490 $tag = $this->read_tag(); 1491 if ($tag == 'smcp') { 1492 $this->hassmallcapsGSUB = true; 1493 } 1494 $Feature[$i] = ['tag' => $tag]; 1495 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort(); 1496 } 1497 1498 for ($i = 0; $i < $FeatureCount; $i++) { 1499 $this->seek($Feature[$i]['offset']); 1500 $this->read_ushort(); // null [FeatureParams] 1501 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort(); 1502 $Feature[$i]['LookupListIndex'] = []; 1503 for ($c = 0; $c < $Lookupcount; $c++) { 1504 $Feature[$i]['LookupListIndex'][] = $this->read_ushort(); 1505 } 1506 } 1507 1508 foreach ($ffeats as $st => $scripts) { 1509 foreach ($scripts as $t => $o) { 1510 $FeatureIndex = $ffeats[$st][$t]; 1511 foreach ($FeatureIndex as $k => $fi) { 1512 $ffeats[$st][$t][$k] = $Feature[$fi]; 1513 } 1514 } 1515 } 1516 1517 $gsub = []; 1518 $GSUBScriptLang = []; 1519 foreach ($ffeats as $st => $scripts) { 1520 foreach ($scripts as $t => $langsys) { 1521 $lg = []; 1522 foreach ($langsys as $ft) { 1523 $lg[$ft['LookupListIndex'][0]] = $ft; 1524 } 1525 // list of Lookups in order they need to be run i.e. order listed in Lookup table 1526 ksort($lg); 1527 foreach ($lg as $ft) { 1528 $gsub[$st][$t][$ft['tag']] = $ft['LookupListIndex']; 1529 } 1530 if (!isset($GSUBScriptLang[$st])) { 1531 $GSUBScriptLang[$st] = ''; 1532 } 1533 $GSUBScriptLang[$st] .= $t . ' '; 1534 } 1535 } 1536 1537 // Get metadata and offsets for whole Lookup List table 1538 $this->seek($LookupList_offset); 1539 $LookupCount = $this->read_ushort(); 1540 $GSLookup = []; 1541 $Offsets = []; 1542 $SubtableCount = []; 1543 1544 for ($i = 0; $i < $LookupCount; $i++) { 1545 $Offsets[$i] = $LookupList_offset + $this->read_ushort(); 1546 } 1547 1548 for ($i = 0; $i < $LookupCount; $i++) { 1549 1550 $this->seek($Offsets[$i]); 1551 1552 $GSLookup[$i]['Type'] = $this->read_ushort(); 1553 $GSLookup[$i]['Flag'] = $flag = $this->read_ushort(); 1554 $GSLookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort(); 1555 1556 for ($c = 0; $c < $SubtableCount[$i]; $c++) { 1557 $GSLookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort(); 1558 } 1559 1560 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure 1561 if (($flag & 0x0010) == 0x0010) { 1562 $GSLookup[$i]['MarkFilteringSet'] = $this->read_ushort(); 1563 } else { 1564 $GSLookup[$i]['MarkFilteringSet'] = ''; 1565 } 1566 1567 // Lookup Type 7: Extension 1568 if ($GSLookup[$i]['Type'] == 7) { 1569 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type 1570 for ($c = 0; $c < $SubtableCount[$i]; $c++) { 1571 $this->seek($GSLookup[$i]['Subtables'][$c]); 1572 $ExtensionPosFormat = $this->read_ushort(); 1573 $type = $this->read_ushort(); 1574 $ext_offset = $this->read_ulong(); 1575 $GSLookup[$i]['Subtables'][$c] = $GSLookup[$i]['Subtables'][$c] + $ext_offset; 1576 } 1577 $GSLookup[$i]['Type'] = $type; 1578 } 1579 } 1580 1581 // Process Whole LookupList - Get LuCoverage = Lookup coverage just for first glyph 1582 $this->GSLuCoverage = []; 1583 for ($i = 0; $i < $LookupCount; $i++) { 1584 for ($c = 0; $c < $GSLookup[$i]['SubtableCount']; $c++) { 1585 $this->seek($GSLookup[$i]['Subtables'][$c]); 1586 $PosFormat = $this->read_ushort(); 1587 1588 if ($GSLookup[$i]['Type'] == 5 && $PosFormat == 3) { 1589 $this->skip(4); 1590 } elseif ($GSLookup[$i]['Type'] == 6 && $PosFormat == 3) { 1591 $BacktrackGlyphCount = $this->read_ushort(); 1592 $this->skip(2 * $BacktrackGlyphCount + 2); 1593 } 1594 1595 // NB Coverage only looks at glyphs for position 1 (i.e. 5.3 and 6.3) // NEEDS TO READ ALL ******************** 1596 $Coverage = $GSLookup[$i]['Subtables'][$c] + $this->read_ushort(); 1597 $this->seek($Coverage); 1598 $glyphs = $this->_getCoverage(false, 2); 1599 $this->GSLuCoverage[$i][$c] = $glyphs; 1600 } 1601 } 1602 1603 // $this->GSLuCoverage and $GSLookup 1604 $this->fontCache->jsonWrite($this->fontkey . '.GSUBdata.json', $this->GSLuCoverage); 1605 1606 // Now repeats as original to get Substitution rules 1607 // Get metadata and offsets for whole Lookup List table 1608 $this->seek($LookupList_offset); 1609 $LookupCount = $this->read_ushort(); 1610 $Lookup = []; 1611 1612 for ($i = 0; $i < $LookupCount; $i++) { 1613 $Lookup[$i]['offset'] = $LookupList_offset + $this->read_ushort(); 1614 } 1615 1616 for ($i = 0; $i < $LookupCount; $i++) { 1617 $this->seek($Lookup[$i]['offset']); 1618 $Lookup[$i]['Type'] = $this->read_ushort(); 1619 $Lookup[$i]['Flag'] = $flag = $this->read_ushort(); 1620 $Lookup[$i]['SubtableCount'] = $this->read_ushort(); 1621 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1622 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['offset'] + $this->read_ushort(); 1623 } 1624 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure 1625 if (($flag & 0x0010) == 0x0010) { 1626 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort(); 1627 } else { 1628 $Lookup[$i]['MarkFilteringSet'] = ''; 1629 } 1630 1631 // Lookup Type 7: Extension 1632 if ($Lookup[$i]['Type'] == 7) { 1633 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type 1634 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1635 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']); 1636 $ExtensionPosFormat = $this->read_ushort(); 1637 $type = $this->read_ushort(); 1638 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ulong(); 1639 } 1640 $Lookup[$i]['Type'] = $type; 1641 } 1642 } 1643 1644 // Process (1) Whole LookupList 1645 for ($i = 0; $i < $LookupCount; $i++) { 1646 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1647 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']); 1648 $SubstFormat = $this->read_ushort(); 1649 $Lookup[$i]['Subtable'][$c]['Format'] = $SubstFormat; 1650 1651 /* 1652 Lookup['Type'] Enumeration table for glyph substitution 1653 Value Type Description 1654 1 Single Replace one glyph with one glyph 1655 2 Multiple Replace one glyph with more than one glyph 1656 3 Alternate Replace one glyph with one of many glyphs 1657 4 Ligature Replace multiple glyphs with one glyph 1658 5 Context Replace one or more glyphs in context 1659 6 Chaining Context Replace one or more glyphs in chained context 1660 7 Extension Substitution Extension mechanism for other substitutions (i.e. this excludes the Extension type substitution itself) 1661 8 Reverse chaining context single Applied in reverse order, replace single glyph in chaining context 1662 */ 1663 1664 // LookupType 1: Single Substitution Subtable 1665 if ($Lookup[$i]['Type'] == 1) { 1666 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1667 if ($SubstFormat == 1) { // Calculated output glyph indices 1668 $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'] = $this->read_short(); 1669 } elseif ($SubstFormat == 2) { // Specified output glyph indices 1670 $GlyphCount = $this->read_ushort(); 1671 for ($g = 0; $g < $GlyphCount; $g++) { 1672 $Lookup[$i]['Subtable'][$c]['Glyphs'][] = $this->read_ushort(); 1673 } 1674 } 1675 } // LookupType 2: Multiple Substitution Subtable 1676 elseif ($Lookup[$i]['Type'] == 2) { 1677 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1678 $Lookup[$i]['Subtable'][$c]['SequenceCount'] = $SequenceCount = $this->read_short(); 1679 for ($s = 0; $s < $SequenceCount; $s++) { 1680 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1681 } 1682 for ($s = 0; $s < $SequenceCount; $s++) { 1683 // Sequence Tables 1684 $this->seek($Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset']); 1685 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount'] = $this->read_short(); 1686 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount']; $g++) { 1687 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['SubstituteGlyphID'][] = $this->read_ushort(); 1688 } 1689 } 1690 } // LookupType 3: Alternate Forms 1691 elseif ($Lookup[$i]['Type'] == 3) { 1692 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1693 $Lookup[$i]['Subtable'][$c]['AlternateSetCount'] = $AlternateSetCount = $this->read_short(); 1694 for ($s = 0; $s < $AlternateSetCount; $s++) { 1695 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1696 } 1697 1698 for ($s = 0; $s < $AlternateSetCount; $s++) { 1699 // AlternateSet Tables 1700 $this->seek($Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset']); 1701 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount'] = $this->read_short(); 1702 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount']; $g++) { 1703 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['SubstituteGlyphID'][] = $this->read_ushort(); 1704 } 1705 } 1706 } // LookupType 4: Ligature Substitution Subtable 1707 elseif ($Lookup[$i]['Type'] == 4) { 1708 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1709 $Lookup[$i]['Subtable'][$c]['LigSetCount'] = $LigSetCount = $this->read_short(); 1710 for ($s = 0; $s < $LigSetCount; $s++) { 1711 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1712 } 1713 for ($s = 0; $s < $LigSetCount; $s++) { 1714 // LigatureSet Tables 1715 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset']); 1716 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount'] = $this->read_short(); 1717 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) { 1718 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g] = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] + $this->read_ushort(); 1719 } 1720 } 1721 for ($s = 0; $s < $LigSetCount; $s++) { 1722 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) { 1723 // Ligature tables 1724 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g]); 1725 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph'] = $this->read_ushort(); 1726 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount'] = $this->read_ushort(); 1727 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) { 1728 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l] = $this->read_ushort(); 1729 } 1730 } 1731 } 1732 } // LookupType 5: Contextual Substitution Subtable 1733 elseif ($Lookup[$i]['Type'] == 5) { 1734 // Format 1: Context Substitution 1735 if ($SubstFormat == 1) { 1736 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1737 $Lookup[$i]['Subtable'][$c]['SubRuleSetCount'] = $SubRuleSetCount = $this->read_short(); 1738 for ($s = 0; $s < $SubRuleSetCount; $s++) { 1739 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short(); 1740 } 1741 for ($s = 0; $s < $SubRuleSetCount; $s++) { 1742 // SubRuleSet Tables 1743 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset']); 1744 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount'] = $this->read_short(); 1745 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) { 1746 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] + $this->read_ushort(); 1747 } 1748 } 1749 for ($s = 0; $s < $SubRuleSetCount; $s++) { 1750 // SubRule Tables 1751 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) { 1752 // Ligature tables 1753 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g]); 1754 1755 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount'] = $this->read_ushort(); 1756 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount'] = $this->read_ushort(); 1757 // "Input"::[GlyphCount - 1]::Array of input GlyphIDs-start with second glyph 1758 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount']; $l++) { 1759 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['Input'][$l] = $this->read_ushort(); 1760 } 1761 // "SubstLookupRecord"::[SubstCount]::Array of SubstLookupRecords-in design order 1762 for ($l = 0; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount']; $l++) { 1763 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['SequenceIndex'] = $this->read_ushort(); 1764 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['LookupListIndex'] = $this->read_ushort(); 1765 } 1766 } 1767 } 1768 } // Format 2: Class-based Context Glyph Substitution 1769 elseif ($SubstFormat == 2) { 1770 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1771 $Lookup[$i]['Subtable'][$c]['ClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1772 $Lookup[$i]['Subtable'][$c]['SubClassSetCnt'] = $this->read_ushort(); 1773 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $b++) { 1774 $offset = $this->read_ushort(); 1775 if ($offset == 0x0000) { 1776 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = 0; 1777 } else { 1778 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset; 1779 } 1780 } 1781 } else { 1782 throw new \Mpdf\Exception\FontException("GPOS Lookup Type " . $Lookup[$i]['Type'] . ", Format " . $SubstFormat . " not supported (ttfontsuni.php)."); 1783 } 1784 } // LookupType 6: Chaining Contextual Substitution Subtable 1785 elseif ($Lookup[$i]['Type'] == 6) { 1786 // Format 1: Simple Chaining Context Glyph Substitution p255 1787 if ($SubstFormat == 1) { 1788 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1789 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount'] = $this->read_ushort(); 1790 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $b++) { 1791 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1792 } 1793 } // Format 2: Class-based Chaining Context Glyph Substitution p257 1794 elseif ($SubstFormat == 2) { 1795 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1796 $Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1797 $Lookup[$i]['Subtable'][$c]['InputClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1798 $Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1799 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt'] = $this->read_ushort(); 1800 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $b++) { 1801 $offset = $this->read_ushort(); 1802 if ($offset == 0x0000) { 1803 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $offset; 1804 } else { 1805 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset; 1806 } 1807 } 1808 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259 1809 elseif ($SubstFormat == 3) { 1810 $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount'] = $this->read_ushort(); 1811 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) { 1812 $Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1813 } 1814 $Lookup[$i]['Subtable'][$c]['InputGlyphCount'] = $this->read_ushort(); 1815 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) { 1816 $Lookup[$i]['Subtable'][$c]['CoverageInput'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1817 } 1818 $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount'] = $this->read_ushort(); 1819 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) { 1820 $Lookup[$i]['Subtable'][$c]['CoverageLookahead'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort(); 1821 } 1822 $Lookup[$i]['Subtable'][$c]['SubstCount'] = $this->read_ushort(); 1823 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) { 1824 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'] = $this->read_ushort(); 1825 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'] = $this->read_ushort(); 1826 // Substitution Lookup Record 1827 // All contextual substitution subtables specify the substitution data in a Substitution Lookup Record 1828 // (SubstLookupRecord). Each record contains a SequenceIndex, which indicates the position where the substitution 1829 // will occur in the glyph sequence. In addition, a LookupListIndex identifies the lookup to be applied at the 1830 // glyph position specified by the SequenceIndex. 1831 } 1832 } 1833 } else { 1834 throw new \Mpdf\Exception\FontException(sprintf('Lookup Type "%s" not supported.', $Lookup[$i]['Type'])); 1835 } 1836 } 1837 } 1838 1839 // Process (2) Whole LookupList 1840 // Get Coverage tables and prepare preg_replace 1841 for ($i = 0; $i < $LookupCount; $i++) { 1842 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 1843 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format']; 1844 1845 // LookupType 1: Single Substitution Subtable 1 => 1 1846 if ($Lookup[$i]['Type'] == 1) { 1847 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1848 $glyphs = $this->_getCoverage(false); 1849 for ($g = 0; $g < count($glyphs); $g++) { 1850 $replace = []; 1851 $substitute = []; 1852 $replace[] = unicode_hex($this->glyphToChar[$glyphs[$g]][0]); 1853 // Flag = Ignore 1854 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1855 continue; 1856 } 1857 if (isset($Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])) { // Format 1 1858 $substitute[] = unicode_hex($this->glyphToChar[($glyphs[$g] + $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])][0]); 1859 } else { // Format 2 1860 $substitute[] = unicode_hex($this->glyphToChar[($Lookup[$i]['Subtable'][$c]['Glyphs'][$g])][0]); 1861 } 1862 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute]; 1863 } 1864 } // LookupType 2: Multiple Substitution Subtable 1 => n 1865 elseif ($Lookup[$i]['Type'] == 2) { 1866 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1867 $glyphs = $this->_getCoverage(); 1868 for ($g = 0; $g < count($glyphs); $g++) { 1869 $replace = []; 1870 $substitute = []; 1871 $replace[] = $glyphs[$g]; 1872 // Flag = Ignore 1873 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1874 continue; 1875 } 1876 if (!isset($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) || count($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) == 0) { 1877 continue; 1878 } // Illegal for GlyphCount to be 0; either error in font, or something has gone wrong - lets carry on for now! 1879 foreach ($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID'] as $sub) { 1880 $substitute[] = unicode_hex($this->glyphToChar[$sub][0]); 1881 } 1882 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute]; 1883 } 1884 } // LookupType 3: Alternate Forms 1 => 1 (only first alternate form is used) 1885 elseif ($Lookup[$i]['Type'] == 3) { 1886 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1887 $glyphs = $this->_getCoverage(); 1888 for ($g = 0; $g < count($glyphs); $g++) { 1889 $replace = []; 1890 $substitute = []; 1891 $replace[] = $glyphs[$g]; 1892 // Flag = Ignore 1893 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1894 continue; 1895 } 1896 $gid = $Lookup[$i]['Subtable'][$c]['AlternateSets'][$g]['SubstituteGlyphID'][0]; 1897 if (!isset($this->glyphToChar[$gid][0])) { 1898 continue; 1899 } 1900 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]); 1901 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute]; 1902 } 1903 } // LookupType 4: Ligature Substitution Subtable n => 1 1904 elseif ($Lookup[$i]['Type'] == 4) { 1905 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1906 $glyphs = $this->_getCoverage(); 1907 $LigSetCount = $Lookup[$i]['Subtable'][$c]['LigSetCount']; 1908 for ($s = 0; $s < $LigSetCount; $s++) { 1909 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) { 1910 $replace = []; 1911 $substitute = []; 1912 $replace[] = $glyphs[$s]; 1913 // Flag = Ignore 1914 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) { 1915 continue; 1916 } 1917 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) { 1918 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l]; 1919 $rpl = unicode_hex($this->glyphToChar[$gid][0]); 1920 // Flag = Ignore 1921 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $rpl, $Lookup[$i]['MarkFilteringSet'])) { 1922 continue 2; 1923 } 1924 $replace[] = $rpl; 1925 } 1926 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph']; 1927 if (!isset($this->glyphToChar[$gid][0])) { 1928 continue; 1929 } 1930 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]); 1931 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute, 'CompCount' => $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']]; 1932 } 1933 } 1934 } // LookupType 5: Contextual Substitution Subtable 1935 elseif ($Lookup[$i]['Type'] == 5) { 1936 // Format 1: Context Substitution 1937 if ($SubstFormat == 1) { 1938 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1939 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 1940 1941 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) { 1942 $SubRuleSet = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]; 1943 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph'] = $CoverageGlyphs[$s]; 1944 for ($r = 0; $r < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $r++) { 1945 $GlyphCount = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['GlyphCount']; 1946 for ($g = 1; $g < $GlyphCount; $g++) { 1947 $glyphID = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['Input'][$g]; 1948 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 1949 } 1950 } 1951 } 1952 } // Format 2: Class-based Context Glyph Substitution 1953 elseif ($SubstFormat == 2) { 1954 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 1955 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 1956 1957 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['ClassDefOffset']); 1958 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses; 1959 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) { 1960 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) { 1961 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s]); 1962 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt'] = $SubClassRuleCnt = $this->read_ushort(); 1963 $SubClassRule = []; 1964 for ($b = 0; $b < $SubClassRuleCnt; $b++) { 1965 $SubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] + $this->read_ushort(); 1966 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $SubClassRule[$b]; 1967 } 1968 } 1969 } 1970 1971 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) { 1972 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) { 1973 $SubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt']; 1974 for ($b = 0; $b < $SubClassRuleCnt; $b++) { 1975 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b]); 1976 $Rule = []; 1977 $Rule['InputGlyphCount'] = $this->read_ushort(); 1978 $Rule['SubstCount'] = $this->read_ushort(); 1979 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) { 1980 $Rule['Input'][$r] = $this->read_ushort(); 1981 } 1982 for ($r = 0; $r < $Rule['SubstCount']; $r++) { 1983 $Rule['SequenceIndex'][$r] = $this->read_ushort(); 1984 $Rule['LookupListIndex'][$r] = $this->read_ushort(); 1985 } 1986 1987 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $Rule; 1988 } 1989 } 1990 } 1991 } // Format 3: Coverage-based Context Glyph Substitution 1992 elseif ($SubstFormat == 3) { 1993 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) { 1994 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]); 1995 $glyphs = $this->_getCoverage(); 1996 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs); 1997 } 1998 throw new \Mpdf\Exception\FontException("Lookup Type 5, SubstFormat 3 not tested. Please report this with the name of font used - " . $this->fontkey); 1999 } 2000 } // LookupType 6: Chaining Contextual Substitution Subtable 2001 elseif ($Lookup[$i]['Type'] == 6) { 2002 // Format 1: Simple Chaining Context Glyph Substitution p255 2003 if ($SubstFormat == 1) { 2004 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 2005 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 2006 2007 $ChainSubRuleSetCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; 2008 2009 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) { 2010 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s]); 2011 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount'] = $this->read_ushort(); 2012 for ($r = 0; $r < $ChainSubRuleCnt; $r++) { 2013 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r] = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s] + $this->read_ushort(); 2014 } 2015 } 2016 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) { 2017 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount']; 2018 for ($r = 0; $r < $ChainSubRuleCnt; $r++) { 2019 // ChainSubRule 2020 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r]); 2021 2022 $BacktrackGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphCount'] = $this->read_ushort(); 2023 for ($g = 0; $g < $BacktrackGlyphCount; $g++) { 2024 $glyphID = $this->read_ushort(); 2025 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 2026 } 2027 2028 $InputGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphCount'] = $this->read_ushort(); 2029 for ($g = 1; $g < $InputGlyphCount; $g++) { 2030 $glyphID = $this->read_ushort(); 2031 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 2032 } 2033 2034 $LookaheadGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphCount'] = $this->read_ushort(); 2035 for ($g = 0; $g < $LookaheadGlyphCount; $g++) { 2036 $glyphID = $this->read_ushort(); 2037 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]); 2038 } 2039 2040 $SubstCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SubstCount'] = $this->read_ushort(); 2041 for ($lu = 0; $lu < $SubstCount; $lu++) { 2042 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SequenceIndex'][$lu] = $this->read_ushort(); 2043 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookupListIndex'][$lu] = $this->read_ushort(); 2044 } 2045 } 2046 } 2047 } // Format 2: Class-based Chaining Context Glyph Substitution p257 2048 elseif ($SubstFormat == 2) { 2049 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']); 2050 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage(); 2051 2052 $BacktrackClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset']); 2053 $Lookup[$i]['Subtable'][$c]['BacktrackClasses'] = $BacktrackClasses; 2054 2055 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['InputClassDefOffset']); 2056 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses; 2057 2058 $LookaheadClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset']); 2059 $Lookup[$i]['Subtable'][$c]['LookaheadClasses'] = $LookaheadClasses; 2060 2061 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) { 2062 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) { 2063 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s]); 2064 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'] = $ChainSubClassRuleCnt = $this->read_ushort(); 2065 $ChainSubClassRule = []; 2066 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) { 2067 $ChainSubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] + $this->read_ushort(); 2068 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $ChainSubClassRule[$b]; 2069 } 2070 } 2071 } 2072 2073 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) { 2074 if (isset($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'])) { 2075 $ChainSubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt']; 2076 } else { 2077 $ChainSubClassRuleCnt = 0; 2078 } 2079 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) { 2080 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) { 2081 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b]); 2082 $Rule = []; 2083 $Rule['BacktrackGlyphCount'] = $this->read_ushort(); 2084 for ($r = 0; $r < $Rule['BacktrackGlyphCount']; $r++) { 2085 $Rule['Backtrack'][$r] = $this->read_ushort(); 2086 } 2087 $Rule['InputGlyphCount'] = $this->read_ushort(); 2088 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) { 2089 $Rule['Input'][$r] = $this->read_ushort(); 2090 } 2091 $Rule['LookaheadGlyphCount'] = $this->read_ushort(); 2092 for ($r = 0; $r < $Rule['LookaheadGlyphCount']; $r++) { 2093 $Rule['Lookahead'][$r] = $this->read_ushort(); 2094 } 2095 $Rule['SubstCount'] = $this->read_ushort(); 2096 for ($r = 0; $r < $Rule['SubstCount']; $r++) { 2097 $Rule['SequenceIndex'][$r] = $this->read_ushort(); 2098 $Rule['LookupListIndex'][$r] = $this->read_ushort(); 2099 } 2100 2101 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $Rule; 2102 } 2103 } 2104 } 2105 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259 2106 elseif ($SubstFormat == 3) { 2107 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) { 2108 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][$b]); 2109 $glyphs = $this->_getCoverage(); 2110 $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'][] = implode("|", $glyphs); 2111 } 2112 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) { 2113 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]); 2114 $glyphs = $this->_getCoverage(); 2115 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs); 2116 // Don't use above value as these are ordered numerically not as need to process 2117 } 2118 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) { 2119 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageLookahead'][$b]); 2120 $glyphs = $this->_getCoverage(); 2121 $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'][] = implode("|", $glyphs); 2122 } 2123 } 2124 } 2125 } 2126 } 2127 2128 $GSUBScriptLang = []; 2129 $rtlpua = []; // All glyphs added to PUA [for magic_reverse] 2130 foreach ($gsub as $st => $scripts) { 2131 foreach ($scripts as $t => $langsys) { 2132 $lul = []; // array of LookupListIndexes 2133 $tags = []; // corresponding array of feature tags e.g. 'ccmp' 2134 2135 foreach ($langsys as $tag => $ft) { 2136 foreach ($ft as $ll) { 2137 $lul[$ll] = $tag; 2138 } 2139 } 2140 ksort($lul); // Order the Lookups in the order they are in the GUSB table, regardless of Feature order 2141 $volt = $this->_getGSUBarray($Lookup, $lul, $st); 2142 2143 // Interrogate $volt 2144 // isol, fin, medi, init(arab syrc) into $rtlSUB for use in ArabJoin 2145 // but also identify all RTL chars in PUA for magic_reverse (arab syrc hebr thaa nko samr) 2146 // identify reph, matras, vatu, half forms etc for Indic for final re-ordering 2147 $rtl = []; 2148 $rtlSUB = []; 2149 $finals = ''; 2150 2151 if (strpos('arab syrc hebr thaa nko samr', $st) !== false) { // all RTL scripts [any/all languages] ? Mandaic 2152 2153 foreach ($volt as $v) { 2154 // isol fina fin2 fin3 medi med2 for Syriac 2155 // ISOLATED FORM :: FINAL :: INITIAL :: MEDIAL :: MED2 :: FIN2 :: FIN3 2156 if (strpos('isol fina init medi fin2 fin3 med2', $v['tag']) !== false) { 2157 2158 $key = $v['match']; 2159 $key = preg_replace('/[\(\)]*/', '', $key); 2160 $sub = $v['replace']; 2161 if ($v['tag'] === 'isol') { 2162 $kk = 0; 2163 } elseif ($v['tag'] === 'fina') { 2164 $kk = 1; 2165 } elseif ($v['tag'] === 'init') { 2166 $kk = 2; 2167 } elseif ($v['tag'] === 'medi') { 2168 $kk = 3; 2169 } elseif ($v['tag'] === 'med2') { 2170 $kk = 4; 2171 } elseif ($v['tag'] === 'fin2') { 2172 $kk = 5; 2173 } elseif ($v['tag'] === 'fin3') { 2174 $kk = 6; 2175 } 2176 2177 $rtl[$key][$kk] = $sub; 2178 if (isset($v['prel']) && count($v['prel'])) { 2179 $rtl[$key]['prel'][$kk] = $v['prel']; 2180 } 2181 if (isset($v['postl']) && count($v['postl'])) { 2182 $rtl[$key]['postl'][$kk] = $v['postl']; 2183 } 2184 if (isset($v['ignore']) && $v['ignore']) { 2185 $rtl[$key]['ignore'][$kk] = $v['ignore']; 2186 } 2187 $rtlpua[] = $sub; 2188 2189 } else { // Add any other glyphs which are in PUA 2190 if (isset($v['context']) && $v['context']) { 2191 foreach ($v['rules'] as $vs) { 2192 $matchCount = count($vs['match']); 2193 for ($i = 0; $i < $matchCount; $i++) { 2194 if (isset($vs['replace'][$i]) && preg_match('/^0[A-F0-9]{4}$/', $vs['match'][$i])) { 2195 if (preg_match('/^0[EF][A-F0-9]{3}$/', $vs['replace'][$i])) { 2196 $rtlpua[] = $vs['replace'][$i]; 2197 } 2198 } 2199 } 2200 } 2201 } else { 2202 preg_match_all('/\((0[A-F0-9]{4})\)/', $v['match'], $m); 2203 $matchCount = count($m[0]); 2204 for ($i = 0; $i < $matchCount; $i++) { 2205 $sb = explode(' ', $v['replace']); 2206 foreach ($sb as $sbg) { 2207 if (preg_match('/(0[EF][A-F0-9]{3})/', $sbg, $mr)) { 2208 $rtlpua[] = $mr[1]; 2209 } 2210 } 2211 } 2212 } 2213 } 2214 } 2215 2216 // For kashida, need to determine all final forms except ones already identified by kashida priority rules (see \Mpdf\Otl) 2217 foreach ($rtl as $base => $variants) { 2218 if (isset($variants[1])) { // i.e. final form 2219 if (strpos('0FE8E 0FE94 0FEA2 0FEAA 0FEAE 0FEC2 0FEDA 0FEDE 0FB93 0FECA 0FED2 0FED6 0FEEE 0FEF0 0FEF2', $variants[1]) === false) { // not already included 2220 // This version does not exclude RA (0631) FEAE; Ya (064A) FEF2; Alef Maqsurah (0649) FEF0 which 2221 // are selected in priority if connected to a medial Bah 2222 //if (strpos('0FE8E 0FE94 0FEA2 0FEAA 0FEC2 0FEDA 0FEDE 0FB93 0FECA 0FED2 0FED6 0FEEE', $variants[1])===false) { // not already included 2223 $finals .= $variants[1] . ' '; 2224 } 2225 } 2226 } 2227 2228 ksort($rtl); 2229 $rtlSUB = $rtl; 2230 } 2231 2232 // INDIC - Dynamic properties 2233 $rphf = []; 2234 $half = []; 2235 $pref = []; 2236 $blwf = []; 2237 $pstf = []; 2238 2239 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] 2240 if (strpos('deva beng guru gujr orya taml telu knda mlym', $st) !== false) { 2241 $is_old_spec = true; 2242 } else { 2243 $is_old_spec = false; 2244 } 2245 2246 // First get 'locl' substitutions (reversed!) 2247 $loclsubs = []; 2248 foreach ($volt as $v) { 2249 if (strpos('locl', $v['tag']) !== false) { 2250 $key = $v['match']; 2251 $key = preg_replace('/[\(\)]*/', '', $key); 2252 $sub = $v['replace']; 2253 if ($key && strlen(trim($key)) == 5 && $sub) { 2254 $loclsubs[$sub] = $key; 2255 } 2256 } 2257 } 2258 2259 foreach ($volt as $v) { 2260 // <rphf> <half> <pref> <blwf> <pstf> 2261 // defines consonant types: 2262 // Reph <rphf> 2263 // Half forms <half> 2264 // Pre-base-reordering forms of Ra/Rra <pref> 2265 // Below-base forms <blwf> 2266 // Post-base forms <pstf> 2267 // applied together with <locl> feature to input sequences consisting of two characters 2268 // This is done for each consonant 2269 // for <rphf> and <half>, features are applied to Consonant + Halant combinations 2270 // for <pref>, <blwf> and <pstf>, features are applied to Halant + Consonant combinations 2271 // Old version eg 'deva' <pref>, <blwf> and <pstf>, features are applied to Consonant + Halant 2272 // Some malformed fonts still do Consonant + Halant for these - so match both?? 2273 // If these two glyphs form a ligature, with no additional glyphs in context 2274 // this means the consonant has the corresponding form 2275 // Currently set to cope with both 2276 // See also classes/otl.php 2277 2278 if (strpos('rphf half pref blwf pstf', $v['tag']) !== false) { 2279 if (isset($v['context']) && $v['context'] && $v['nBacktrack'] == 0 && $v['nLookahead'] == 0) { 2280 foreach ($v['rules'] as $vs) { 2281 if (count($vs['match']) == 2 && count($vs['replace']) == 1) { 2282 $sub = $vs['replace'][0]; 2283 // If Halant Cons <pref>, <blwf> and <pstf> in New version only 2284 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) { 2285 $key = $vs['match'][1]; 2286 $tag = $v['tag']; 2287 if (isset($loclsubs[$key])) { 2288 ${$tag[$loclsubs[$key]]} = $sub; 2289 } 2290 $tmp = &$$tag; 2291 $tmp[hexdec($key)] = hexdec($sub); 2292 } // If Cons Halant <rphf> and <half> always 2293 // and <pref>, <blwf> and <pstf> in Old version 2294 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)))) { 2295 $key = $vs['match'][0]; 2296 $tag = $v['tag']; 2297 if (isset($loclsubs[$key])) { 2298 ${$tag[$loclsubs[$key]]} = $sub; 2299 } 2300 $tmp = &$$tag; 2301 $tmp[hexdec($key)] = hexdec($sub); 2302 } 2303 } 2304 } 2305 } elseif (!isset($v['context'])) { 2306 $key = $v['match']; 2307 $key = preg_replace('/[\(\)]*/', '', $key); 2308 $sub = $v['replace']; 2309 if ($key && strlen(trim($key)) == 11 && $sub) { 2310 // If Cons Halant <rphf> and <half> always 2311 // and <pref>, <blwf> and <pstf> in Old version 2312 // If Halant Cons <pref>, <blwf> and <pstf> in New version only 2313 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) { 2314 $key = substr($key, 6, 5); 2315 $tag = $v['tag']; 2316 if (isset($loclsubs[$key])) { 2317 ${$tag[$loclsubs[$key]]} = $sub; 2318 } 2319 $tmp = &$$tag; 2320 $tmp[hexdec($key)] = hexdec($sub); 2321 } 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)))) { 2322 $key = substr($key, 0, 5); 2323 $tag = $v['tag']; 2324 if (isset($loclsubs[$key])) { 2325 ${$tag[$loclsubs[$key]]} = $sub; 2326 } 2327 $tmp = &$$tag; 2328 $tmp[hexdec($key)] = hexdec($sub); 2329 } 2330 } 2331 } 2332 } 2333 } 2334 } 2335 2336 if (count($rtl) || count($rphf) || count($half) || count($pref) || count($blwf) || count($pstf) || $finals) { 2337 $font = [ 2338 'rtlSUB' => $rtlSUB, 2339 'finals' => $finals, 2340 'rphf' => $rphf, 2341 'half' => $half, 2342 'pref' => $pref, 2343 'blwf' => $blwf, 2344 'pstf' => $pstf, 2345 ]; 2346 2347 $this->fontCache->jsonWrite($this->fontkey . '.GSUB.' . $st . '.' . $t . '.json', $font); 2348 } 2349 2350 if (!isset($GSUBScriptLang[$st])) { 2351 $GSUBScriptLang[$st] = ''; 2352 } 2353 $GSUBScriptLang[$st] .= $t . ' '; 2354 } 2355 } 2356 2357 // All RTL glyphs from font added to (or already in) PUA [reqd for magic_reverse] 2358 $rtlPUAstr = ''; 2359 if (count($rtlpua)) { 2360 $rtlpua = array_unique($rtlpua); 2361 sort($rtlpua); 2362 $n = count($rtlpua); 2363 for ($i = 0; $i < $n; $i++) { 2364 if (hexdec($rtlpua[$i]) < hexdec('E000') || hexdec($rtlpua[$i]) > hexdec('F8FF')) { 2365 unset($rtlpua[$i]); 2366 } 2367 } 2368 sort($rtlpua, SORT_STRING); 2369 2370 $rangeid = -1; 2371 $range = []; 2372 $prevgid = -2; 2373 2374 // for each character 2375 foreach ($rtlpua as $gidhex) { 2376 $gid = hexdec($gidhex); 2377 if ($gid == ($prevgid + 1)) { 2378 $range[$rangeid]['end'] = $gidhex; 2379 $range[$rangeid]['count']++; 2380 } else { 2381 // new range 2382 $rangeid++; 2383 $range[$rangeid] = []; 2384 $range[$rangeid]['start'] = $gidhex; 2385 $range[$rangeid]['end'] = $gidhex; 2386 $range[$rangeid]['count'] = 1; 2387 } 2388 $prevgid = $gid; 2389 } 2390 2391 foreach ($range as $rg) { 2392 if ($rg['count'] == 1) { 2393 $rtlPUAstr .= "\x{" . $rg['start'] . "}"; 2394 } elseif ($rg['count'] == 2) { 2395 $rtlPUAstr .= "\x{" . $rg['start'] . "}\x{" . $rg['end'] . "}"; 2396 } else { 2397 $rtlPUAstr .= "\x{" . $rg['start'] . "}-\x{" . $rg['end'] . "}"; 2398 } 2399 } 2400 } 2401 2402 return [$GSUBScriptLang, $gsub, $GSLookup, $rtlPUAstr]; 2403 } 2404 2405 // GSUB functions 2406 function _getGSUBarray(&$Lookup, &$lul, $scripttag) 2407 { 2408 // Process (3) LookupList for specific Script-LangSys 2409 // Generate preg_replace 2410 $volt = []; 2411 $reph = ''; 2412 $matraE = ''; 2413 $vatu = ''; 2414 2415 foreach ($lul as $i => $tag) { 2416 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) { 2417 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format']; 2418 2419 // LookupType 1: Single Substitution Subtable 2420 if ($Lookup[$i]['Type'] == 1) { 2421 $subCount = count($Lookup[$i]['Subtable'][$c]['subs']); 2422 for ($s = 0; $s < $subCount; $s++) { 2423 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2424 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; 2425 // Ignore has already been applied earlier on 2426 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()"); 2427 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0); 2428 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 1]; 2429 } 2430 } // LookupType 2: Multiple Substitution Subtable 2431 elseif ($Lookup[$i]['Type'] == 2) { 2432 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { 2433 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2434 $substitute = implode(" ", $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute']); 2435 // Ignore has already been applied earlier on 2436 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()"); 2437 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0); 2438 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 2]; 2439 } 2440 } // LookupType 3: Alternate Forms 2441 elseif ($Lookup[$i]['Type'] == 3) { 2442 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { 2443 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2444 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; 2445 // Ignore has already been applied earlier on 2446 $repl = $this->_makeGSUBinputMatch($inputGlyphs, "()"); 2447 $subs = $this->_makeGSUBinputReplacement(1, $substitute, "()", 0, 1, 0); 2448 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 3]; 2449 } 2450 } // LookupType 4: Ligature Substitution Subtable 2451 elseif ($Lookup[$i]['Type'] == 4) { 2452 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) { 2453 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace']; 2454 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0]; 2455 // Ignore has already been applied earlier on 2456 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2457 $repl = $this->_makeGSUBinputMatch($inputGlyphs, $ignore); 2458 $subs = $this->_makeGSUBinputReplacement(count($inputGlyphs), $substitute, $ignore, 0, count($inputGlyphs), 0); 2459 $volt[] = ['match' => $repl, 'replace' => $subs, 'tag' => $tag, 'key' => $inputGlyphs[0], 'type' => 4, 'CompCount' => $Lookup[$i]['Subtable'][$c]['subs'][$s]['CompCount'], 'Lig' => $substitute]; 2460 } 2461 } // LookupType 5: Chaining Contextual Substitution Subtable 2462 elseif ($Lookup[$i]['Type'] == 5) { 2463 // Format 1: Context Substitution 2464 if ($SubstFormat == 1) { 2465 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2466 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) { 2467 // SubRuleSet 2468 $subRule = []; 2469 foreach ($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'] as $rule) { 2470 // SubRule 2471 $inputGlyphs = []; 2472 if ($rule['GlyphCount'] > 1) { 2473 $inputGlyphs = $rule['InputGlyphs']; 2474 } 2475 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph']; 2476 ksort($inputGlyphs); 2477 $nInput = count($inputGlyphs); 2478 2479 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2480 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => '', 'match' => $contextInputMatch, 'nBacktrack' => 0, 'nInput' => $nInput, 'nLookahead' => 0, 'rules' => [],]; 2481 2482 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2483 $lup = $rule['SubstLookupRecord'][$b]['LookupListIndex']; 2484 $seqIndex = $rule['SubstLookupRecord'][$b]['SequenceIndex']; 2485 2486 // $Lookup[$lup] = secondary Lookup 2487 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2488 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2489 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2490 $lookupGlyphs = $luss['Replace']; 2491 $mLen = count($lookupGlyphs); 2492 2493 // Only apply if the (first) 'Replace' glyph from the 2494 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2495 // then apply the substitution 2496 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2497 continue; 2498 } 2499 $REPL = implode(" ", $luss['substitute']); 2500 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2501 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2502 } else { 2503 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2504 } 2505 } 2506 } 2507 } 2508 } 2509 2510 if (count($subRule['rules'])) { 2511 $volt[] = $subRule; 2512 } 2513 } 2514 } 2515 } // Format 2: Class-based Context Glyph Substitution 2516 elseif ($SubstFormat == 2) { 2517 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2518 foreach ($Lookup[$i]['Subtable'][$c]['SubClassSet'] as $inputClass => $cscs) { 2519 for ($cscrule = 0; $cscrule < $cscs['SubClassRuleCnt']; $cscrule++) { 2520 $rule = $cscs['SubClassRule'][$cscrule]; 2521 2522 $inputGlyphs = []; 2523 2524 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass]; 2525 if ($rule['InputGlyphCount'] > 1) { 2526 // NB starts at 1 2527 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) { 2528 $classindex = $rule['Input'][$gcl]; 2529 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex])) { 2530 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex]; 2531 } // if class[0] = all glyphs excluding those specified in all other classes 2532 // set to blank '' for now 2533 else { 2534 $inputGlyphs[$gcl] = ''; 2535 } 2536 } 2537 } 2538 2539 $nInput = $rule['InputGlyphCount']; 2540 $nIsubs = (2 * $nInput) - 1; 2541 2542 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2543 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => '', 'match' => $contextInputMatch, 'nBacktrack' => 0, 'nInput' => $nInput, 'nLookahead' => 0, 'rules' => [],]; 2544 2545 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2546 $lup = $rule['LookupListIndex'][$b]; 2547 $seqIndex = $rule['SequenceIndex'][$b]; 2548 2549 // $Lookup[$lup] = secondary Lookup 2550 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2551 if (isset($Lookup[$lup]['Subtable'][$lus]['subs']) && count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2552 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2553 $lookupGlyphs = $luss['Replace']; 2554 $mLen = count($lookupGlyphs); 2555 2556 // Only apply if the (first) 'Replace' glyph from the 2557 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2558 // then apply the substitution 2559 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2560 continue; 2561 } 2562 2563 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2564 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2565 $REPL = implode(" ", $luss['substitute']); 2566 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}" 2567 2568 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2569 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2570 } else { 2571 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2572 } 2573 } 2574 } 2575 } 2576 } 2577 if (count($subRule['rules'])) { 2578 $volt[] = $subRule; 2579 } 2580 } 2581 } 2582 2583 } // Format 3: Coverage-based Context Glyph Substitution p259 2584 elseif ($SubstFormat == 3) { 2585 2586 // IgnoreMarks flag set on main Lookup table 2587 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2588 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs']; 2589 $CoverageInputGlyphs = implode('|', $inputGlyphs); 2590 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; 2591 2592 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) { 2593 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs']; 2594 } else { 2595 $backtrackGlyphs = []; 2596 } 2597 2598 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 2599 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2600 2601 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) { 2602 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs']; 2603 } else { 2604 $lookaheadGlyphs = []; 2605 } 2606 2607 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 2608 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2609 2610 $nBsubs = 2 * count($backtrackGlyphs); 2611 $nIsubs = (2 * $nInput) - 1; 2612 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2613 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2614 2615 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) { 2616 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex']; 2617 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex']; 2618 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2619 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2620 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2621 $lookupGlyphs = $luss['Replace']; 2622 $mLen = count($lookupGlyphs); 2623 2624 // Only apply if the (first) 'Replace' glyph from the 2625 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2626 // then apply the substitution 2627 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2628 continue; 2629 } 2630 2631 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2632 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2633 $REPL = implode(" ", $luss['substitute']); 2634 2635 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2636 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2637 } else { 2638 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2639 } 2640 } 2641 } 2642 } 2643 } 2644 if (count($subRule['rules'])) { 2645 $volt[] = $subRule; 2646 } 2647 } 2648 2649 } // LookupType 6: ing Contextual Substitution Subtable 2650 elseif ($Lookup[$i]['Type'] == 6) { 2651 2652 // Format 1: Simple Chaining Context Glyph Substitution p255 2653 if ($SubstFormat == 1) { 2654 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2655 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $s++) { 2656 2657 // ChainSubRuleSet 2658 $subRule = []; 2659 $firstInputGlyph = $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'][$s]; // First input gyyph 2660 2661 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'] as $rule) { 2662 // ChainSubRule 2663 $inputGlyphs = []; 2664 if ($rule['InputGlyphCount'] > 1) { 2665 $inputGlyphs = $rule['InputGlyphs']; 2666 } 2667 $inputGlyphs[0] = $firstInputGlyph; 2668 ksort($inputGlyphs); 2669 $nInput = count($inputGlyphs); 2670 2671 if ($rule['BacktrackGlyphCount']) { 2672 $backtrackGlyphs = $rule['BacktrackGlyphs']; 2673 } else { 2674 $backtrackGlyphs = []; 2675 } 2676 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2677 2678 if ($rule['LookaheadGlyphCount']) { 2679 $lookaheadGlyphs = $rule['LookaheadGlyphs']; 2680 } else { 2681 $lookaheadGlyphs = []; 2682 } 2683 2684 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2685 2686 $nBsubs = 2 * count($backtrackGlyphs); 2687 $nIsubs = (2 * $nInput) - 1; 2688 2689 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2690 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2691 2692 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2693 $lup = $rule['LookupListIndex'][$b]; 2694 $seqIndex = $rule['SequenceIndex'][$b]; 2695 2696 // $Lookup[$lup] = secondary Lookup 2697 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2698 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2699 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2700 $lookupGlyphs = $luss['Replace']; 2701 $mLen = count($lookupGlyphs); 2702 2703 // Only apply if the (first) 'Replace' glyph from the 2704 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2705 // then apply the substitution 2706 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2707 continue; 2708 } 2709 2710 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2711 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2712 2713 $REPL = implode(" ", $luss['substitute']); 2714 2715 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2716 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2717 } else { 2718 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2719 } 2720 } 2721 } 2722 } 2723 } 2724 2725 if (count($subRule['rules'])) { 2726 $volt[] = $subRule; 2727 } 2728 } 2729 } 2730 2731 } // Format 2: Class-based Chaining Context Glyph Substitution p257 2732 elseif ($SubstFormat == 2) { 2733 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2734 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'] as $inputClass => $cscs) { 2735 for ($cscrule = 0; $cscrule < $cscs['ChainSubClassRuleCnt']; $cscrule++) { 2736 $rule = $cscs['ChainSubClassRule'][$cscrule]; 2737 2738 // These contain classes of glyphs as strings 2739 // $Lookup[$i]['Subtable'][$c]['InputClasses'][(class)] e.g. 02E6|02E7|02E8 2740 // $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][(class)] 2741 // $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][(class)] 2742 // These contain arrays of classIndexes 2743 // [Backtrack] [Lookahead] and [Input] (Input is from the second position only) 2744 2745 $inputGlyphs = []; 2746 2747 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass])) { 2748 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass]; 2749 } else { 2750 $inputGlyphs[0] = ''; 2751 } 2752 if ($rule['InputGlyphCount'] > 1) { 2753 // NB starts at 1 2754 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) { 2755 $classindex = $rule['Input'][$gcl]; 2756 if (isset($Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex])) { 2757 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex]; 2758 } // if class[0] = all glyphs excluding those specified in all other classes 2759 // set to blank '' for now 2760 else { 2761 $inputGlyphs[$gcl] = ''; 2762 } 2763 } 2764 } 2765 2766 $nInput = $rule['InputGlyphCount']; 2767 2768 if ($rule['BacktrackGlyphCount']) { 2769 for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) { 2770 $classindex = $rule['Backtrack'][$gcl]; 2771 if (isset($Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex])) { 2772 $backtrackGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex]; 2773 } // if class[0] = all glyphs excluding those specified in all other classes 2774 // set to blank '' for now 2775 else { 2776 $backtrackGlyphs[$gcl] = ''; 2777 } 2778 } 2779 } else { 2780 $backtrackGlyphs = []; 2781 } 2782 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 2783 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2784 2785 if ($rule['LookaheadGlyphCount']) { 2786 for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) { 2787 $classindex = $rule['Lookahead'][$gcl]; 2788 if (isset($Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex])) { 2789 $lookaheadGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex]; 2790 } // if class[0] = all glyphs excluding those specified in all other classes 2791 // set to blank '' for now 2792 else { 2793 $lookaheadGlyphs[$gcl] = ''; 2794 } 2795 } 2796 } else { 2797 $lookaheadGlyphs = []; 2798 } 2799 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 2800 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2801 2802 $nBsubs = 2 * count($backtrackGlyphs); 2803 $nIsubs = (2 * $nInput) - 1; 2804 2805 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2806 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2807 2808 for ($b = 0; $b < $rule['SubstCount']; $b++) { 2809 $lup = $rule['LookupListIndex'][$b]; 2810 $seqIndex = $rule['SequenceIndex'][$b]; 2811 2812 // $Lookup[$lup] = secondary Lookup 2813 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2814 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2815 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2816 $lookupGlyphs = $luss['Replace']; 2817 $mLen = count($lookupGlyphs); 2818 2819 // Only apply if the (first) 'Replace' glyph from the 2820 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2821 // then apply the substitution 2822 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2823 continue; 2824 } 2825 2826 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2827 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2828 $REPL = implode(" ", $luss['substitute']); 2829 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}" 2830 2831 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2832 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2833 } else { 2834 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2835 } 2836 } 2837 } 2838 } 2839 } 2840 if (count($subRule['rules'])) { 2841 $volt[] = $subRule; 2842 } 2843 } 2844 } 2845 2846 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259 2847 elseif ($SubstFormat == 3) { 2848 // IgnoreMarks flag set on main Lookup table 2849 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']); 2850 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs']; 2851 $CoverageInputGlyphs = implode('|', $inputGlyphs); 2852 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; 2853 2854 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) { 2855 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs']; 2856 } else { 2857 $backtrackGlyphs = []; 2858 } 2859 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 2860 $backtrackMatch = $this->_makeGSUBbacktrackMatch($backtrackGlyphs, $ignore); 2861 2862 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) { 2863 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs']; 2864 } else { 2865 $lookaheadGlyphs = []; 2866 } 2867 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 2868 $lookaheadMatch = $this->_makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore); 2869 2870 $nBsubs = 2 * count($backtrackGlyphs); 2871 $nIsubs = (2 * $nInput) - 1; 2872 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, [], 0); 2873 $subRule = ['context' => 1, 'tag' => $tag, 'matchback' => $backtrackMatch, 'match' => ($contextInputMatch . $lookaheadMatch), 'nBacktrack' => count($backtrackGlyphs), 'nInput' => $nInput, 'nLookahead' => count($lookaheadGlyphs), 'rules' => [],]; 2874 2875 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) { 2876 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex']; 2877 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex']; 2878 for ($lus = 0; $lus < $Lookup[$lup]['SubtableCount']; $lus++) { 2879 if (count($Lookup[$lup]['Subtable'][$lus]['subs'])) { 2880 foreach ($Lookup[$lup]['Subtable'][$lus]['subs'] as $luss) { 2881 $lookupGlyphs = $luss['Replace']; 2882 $mLen = count($lookupGlyphs); 2883 2884 // Only apply if the (first) 'Replace' glyph from the 2885 // Lookup list is in the [inputGlyphs] at ['SequenceIndex'] 2886 // then apply the substitution 2887 if (strpos($inputGlyphs[$seqIndex], $lookupGlyphs[0]) === false) { 2888 continue; 2889 } 2890 2891 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 2892 $contextInputMatch = $this->_makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex); 2893 $REPL = implode(" ", $luss['substitute']); 2894 2895 if (strpos("isol fina fin2 fin3 medi med2 init ", $tag) !== false && $scripttag == 'arab') { 2896 $volt[] = ['match' => $lookupGlyphs[0], 'replace' => $REPL, 'tag' => $tag, 'prel' => $backtrackGlyphs, 'postl' => $lookaheadGlyphs, 'ignore' => $ignore]; 2897 } else { 2898 $subRule['rules'][] = ['type' => $Lookup[$lup]['Type'], 'match' => $lookupGlyphs, 'replace' => $luss['substitute'], 'seqIndex' => $seqIndex, 'key' => $lookupGlyphs[0],]; 2899 } 2900 } 2901 } 2902 } 2903 } 2904 if (count($subRule['rules'])) { 2905 $volt[] = $subRule; 2906 } 2907 } 2908 } 2909 } 2910 } 2911 2912 return $volt; 2913 } 2914 2915 function _checkGSUBignore($flag, $glyph, $MarkFilteringSet) 2916 { 2917 $ignore = false; 2918 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType) 2919 if ((($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) && strpos($this->GlyphClassMarks, $glyph)) { 2920 $ignore = true; 2921 } 2922 if ((($flag & 0x0004) == 0x0004) && strpos($this->GlyphClassLigatures, $glyph)) { 2923 $ignore = true; 2924 } 2925 if ((($flag & 0x0002) == 0x0002) && strpos($this->GlyphClassBases, $glyph)) { 2926 $ignore = true; 2927 } 2928 // Flag & 0xFF?? = MarkAttachmentType 2929 if ($flag & 0xFF00) { 2930 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class" 2931 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table 2932 if (strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)) { 2933 $ignore = true; 2934 } 2935 } 2936 // Flag & 0x0010 = UseMarkFilteringSet 2937 if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) { 2938 $ignore = true; 2939 } 2940 2941 return $ignore; 2942 } 2943 2944 function _getGSUBignoreString($flag, $MarkFilteringSet) 2945 { 2946 // If ignoreFlag set, combine all ignore glyphs into -> "((?:(?: FBA1| FBA2| FBA3))*)" 2947 // else "()" 2948 // for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup 2949 $str = ""; 2950 $ignoreflag = 0; 2951 2952 // Flag & 0xFF?? = MarkAttachmentType 2953 if ($flag & 0xFF00) { 2954 // "a lookup must ignore any mark glyphs that are not in the specified mark attachment class" 2955 // $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table 2956 $MarkAttachmentType = $flag >> 8; 2957 $ignoreflag = $flag; 2958 $str = $this->MarkAttachmentType[$MarkAttachmentType]; 2959 } 2960 2961 // Flag & 0x0010 = UseMarkFilteringSet 2962 if ($flag & 0x0010) { 2963 throw new \Mpdf\Exception\FontException("This font " . $this->fontkey . " contains MarkGlyphSets - Not tested yet"); 2964 $str = $this->MarkGlyphSets[$MarkFilteringSet]; 2965 } 2966 2967 // If Ignore Marks set, supercedes any above 2968 // Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType) 2969 if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) { 2970 $ignoreflag = 8; 2971 $str = $this->GlyphClassMarks; 2972 } 2973 2974 // Flag & 0x0004 = Ignore Ligatures 2975 if (($flag & 0x0004) == 0x0004) { 2976 $ignoreflag += 4; 2977 if ($str) { 2978 $str .= "|"; 2979 } 2980 $str .= $this->GlyphClassLigatures; 2981 } 2982 // Flag & 0x0002 = Ignore BaseGlyphs 2983 if (($flag & 0x0002) == 0x0002) { 2984 $ignoreflag += 2; 2985 if ($str) { 2986 $str .= "|"; 2987 } 2988 $str .= $this->GlyphClassBases; 2989 } 2990 if ($str) { 2991 // This originally returned e.g. ((?:(?:[IGNORE8]))*) when NOT specific to a Lookup e.g. rtlSub in 2992 // arabictypesetting.GSUB.arab.DFLT.php 2993 // This would save repeatedly saving long text strings if used multiple times 2994 // When writing e.g. arabictypesetting.GSUB.arab.DFLT.php to file, included as $ignore[8] 2995 // Would need to also write the $ignore array to that file 2996 // // If UseMarkFilteringSet (specific to the Lookup) return the string 2997 // if (($flag & 0x0010) && ($flag & 0x0008) != 0x0008) { 2998 // return "((?:(?:" . $str . "))*)"; 2999 // } 3000 // else { return "((?:(?:" . "[IGNORE".$ignoreflag."]" . "))*)"; } 3001 // // e.g. ((?:(?: 0031| 0032| 0033| 0034| 0045))*) 3002 // 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 3003 // writing. So just output it as a string: 3004 return "((?:(?:" . $str . "))*)"; 3005 } else { 3006 return "()"; 3007 } 3008 } 3009 3010 // GSUB Patterns 3011 3012 /* 3013 BACKTRACK INPUT LOOKAHEAD 3014 ================================== ================== ================================== 3015 (FEEB|FEEC)(ign) ¦(FD12|FD13)(ign) ¦(0612)¦(ign) (0613)¦(ign) (FD12|FD13)¦(ign) (FEEB|FEEC) 3016 ---------------- ---------------- ----- ------------ --------------- --------------- 3017 Backtrack 1 Backtrack 2 Input 1 Input 2 Lookahead 1 Lookahead 2 3018 -------- --- --------- --- ---- --- ---- --- --------- --- ------- 3019 \${1} \${2} \${3} \${4} \${5+} \${6+} \${7+} \${8+} 3020 3021 nBacktrack = 2 nInput = 2 nLookahead = 2 3022 3023 nBsubs = 2xnBack nIsubs = (nBsubs+) nLsubs = (nBsubs+nIsubs+) 2xnLookahead 3024 "\${1}\${2} " (nInput*2)-1 "\${5+} \${6+}" 3025 "REPL" 3026 3027 ¦\${1}\${2} ¦\${3}\${4} ¦REPL¦\${5+} \${6+}¦\${7+} \${8+}¦ 3028 3029 3030 INPUT nInput = 5 3031 ============================================================ 3032 ¦(0612)¦(ign) (0613)¦(ign) (0614)¦(ign) (0615)¦(ign) (0615)¦ 3033 \${1} \${2} \${3} \${4} \${5} \${6} \${7} \${8} \${9} (All backreference numbers are + nBsubs) 3034 ----- ------------ ------------ ------------ ------------ 3035 Input 1 Input 2 Input 3 Input 4 Input 5 3036 3037 A====== SequenceIndex=1 ; Lookup match nGlyphs=1 3038 B=================== SequenceIndex=1 ; Lookup match nGlyphs=2 3039 C=============================== SequenceIndex=1 ; Lookup match nGlyphs=3 3040 D======================= SequenceIndex=2 ; Lookup match nGlyphs=2 3041 E===================================== SequenceIndex=2 ; Lookup match nGlyphs=3 3042 F====================== SequenceIndex=4 ; Lookup match nGlyphs=2 3043 3044 All backreference numbers are + nBsubs 3045 A - "REPL\${2} \${3}\${4} \${5}\${6} \${7}\${8} \${9}" 3046 B - "REPL\${2}\${4} \${5}\${6} \${7}\${8} \${9}" 3047 C - "REPL\${2}\${4}\${6} \${7}\${8} \${9}" 3048 D - "\${1} REPL\${2}\${4}\${6} \${7}\${8} \${9}" 3049 E - "\${1} REPL\${2}\${4}\${6}\${8} \${9}" 3050 F - "\${1}\${2} \${3}\${4} \${5} REPL\${6}\${8}" 3051 */ 3052 3053 function _makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex) 3054 { 3055 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3056 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 3057 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context 3058 // $lookupGlyphs = array of glyphs (single Glyphs) making up Lookup Input sequence 3059 $mLen = count($lookupGlyphs); // nGlyphs in the secondary Lookup match 3060 $nInput = count($inputGlyphs); // nGlyphs in the Primary Input sequence 3061 $str = ""; 3062 for ($i = 0; $i < $nInput; $i++) { 3063 if ($i > 0) { 3064 $str .= $ignore . " "; 3065 } 3066 if ($i >= $seqIndex && $i < ($seqIndex + $mLen)) { 3067 $str .= "(" . $lookupGlyphs[($i - $seqIndex)] . ")"; 3068 } else { 3069 $str .= "(" . $inputGlyphs[($i)] . ")"; 3070 } 3071 } 3072 3073 return $str; 3074 } 3075 3076 function _makeGSUBinputMatch($inputGlyphs, $ignore) 3077 { 3078 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3079 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦ 3080 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context 3081 // $lookupGlyphs = array of glyphs making up Lookup Input sequence - if applicable 3082 $str = ""; 3083 for ($i = 1; $i <= count($inputGlyphs); $i++) { 3084 if ($i > 1) { 3085 $str .= $ignore . " "; 3086 } 3087 $str .= "(" . $inputGlyphs[($i - 1)] . ")"; 3088 } 3089 3090 return $str; 3091 } 3092 3093 function _makeGSUBbacktrackMatch($backtrackGlyphs, $ignore) 3094 { 3095 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3096 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦ 3097 // $backtrackGlyphs = array of glyphstrings making up Backtrack sequence 3098 // 3 2 1 0 3099 // each item being e.g. E0AD|E0AF|F1FD 3100 $str = ""; 3101 for ($i = (count($backtrackGlyphs) - 1); $i >= 0; $i--) { 3102 $str .= "(" . $backtrackGlyphs[$i] . ")" . $ignore . " "; 3103 } 3104 3105 return $str; 3106 } 3107 3108 function _makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore) 3109 { 3110 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3111 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦ 3112 // $lookaheadGlyphs = array of glyphstrings making up Lookahead sequence 3113 // 0 1 2 3 3114 // each item being e.g. E0AD|E0AF|F1FD 3115 $str = ""; 3116 for ($i = 0; $i < count($lookaheadGlyphs); $i++) { 3117 $str .= $ignore . " (" . $lookaheadGlyphs[$i] . ")"; 3118 } 3119 3120 return $str; 3121 } 3122 3123 function _makeGSUBinputReplacement($nInput, $REPL, $ignore, $nBsubs, $mLen, $seqIndex) 3124 { 3125 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}" 3126 // $nInput nGlyphs in the Primary Input sequence 3127 // $REPL replacement glyphs from secondary lookup 3128 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()" 3129 // $nBsubs Number of Backtrack substitutions (= 2x Number of Backtrack glyphs) 3130 // $mLen nGlyphs in the secondary Lookup match - if no secondary lookup, should=$nInput 3131 // $seqIndex Sequence Index to apply the secondary match 3132 if ($ignore == "()") { 3133 $ign = false; 3134 } else { 3135 $ign = true; 3136 } 3137 $str = ""; 3138 if ($nInput == 1) { 3139 $str = $REPL; 3140 } elseif ($nInput > 1) { 3141 if ($mLen == $nInput) { // whole string replaced 3142 $str = $REPL; 3143 if ($ign) { 3144 // for every nInput over 1, add another replacement backreference, to move IGNORES after replacement 3145 for ($x = 2; $x <= $nInput; $x++) { 3146 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3147 } 3148 } 3149 } else { // if only part of string replaced: 3150 for ($x = 1; $x < ($seqIndex + 1); $x++) { 3151 if ($x == 1) { 3152 $str .= '\\' . ($nBsubs + 1); 3153 } else { 3154 if ($ign) { 3155 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3156 } 3157 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1))); 3158 } 3159 } 3160 if ($seqIndex > 0) { 3161 $str .= " "; 3162 } 3163 $str .= $REPL; 3164 if ($ign) { 3165 for ($x = (max(($seqIndex + 1), 2)); $x < ($seqIndex + 1 + $mLen); $x++) { // move IGNORES after replacement 3166 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3167 } 3168 } 3169 for ($x = ($seqIndex + 1 + $mLen); $x <= $nInput; $x++) { 3170 if ($ign) { 3171 $str .= '\\' . ($nBsubs + (2 * ($x - 1))); 3172 } 3173 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1))); 3174 } 3175 } 3176 } 3177 3178 return $str; 3179 } 3180 3181 function _getCoverage($convert2hex = true, $mode = 1) 3182 { 3183 $g = []; 3184 $ctr = 0; 3185 $CoverageFormat = $this->read_ushort(); 3186 if ($CoverageFormat == 1) { 3187 $CoverageGlyphCount = $this->read_ushort(); 3188 for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) { 3189 $glyphID = $this->read_ushort(); 3190 $uni = $this->glyphToChar[$glyphID][0]; 3191 if ($convert2hex) { 3192 $g[] = unicode_hex($uni); 3193 } elseif ($mode == 2) { 3194 $g[$uni] = $ctr; 3195 $ctr++; 3196 } else { 3197 $g[] = $glyphID; 3198 } 3199 } 3200 } 3201 if ($CoverageFormat == 2) { 3202 $RangeCount = $this->read_ushort(); 3203 for ($r = 0; $r < $RangeCount; $r++) { 3204 $start = $this->read_ushort(); 3205 $end = $this->read_ushort(); 3206 $StartCoverageIndex = $this->read_ushort(); // n/a 3207 for ($glyphID = $start; $glyphID <= $end; $glyphID++) { 3208 $uni = $this->glyphToChar[$glyphID][0]; 3209 if ($convert2hex) { 3210 $g[] = unicode_hex($uni); 3211 } elseif ($mode == 2) { 3212 $uni = $g[$uni] = $ctr; 3213 $ctr++; 3214 } else { 3215 $g[] = $glyphID; 3216 } 3217 } 3218 } 3219 } 3220 3221 return $g; 3222 } 3223 3224 function _getClasses($offset) 3225 { 3226 $this->seek($offset); 3227 $ClassFormat = $this->read_ushort(); 3228 $GlyphByClass = []; 3229 if ($ClassFormat == 1) { 3230 $StartGlyph = $this->read_ushort(); 3231 $GlyphCount = $this->read_ushort(); 3232 for ($i = 0; $i < $GlyphCount; $i++) { 3233 $startGlyphID = $StartGlyph + $i; 3234 $endGlyphID = $StartGlyph + $i; 3235 $class = $this->read_ushort(); 3236 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) { 3237 if (isset($this->glyphToChar[$g][0])) { 3238 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]); 3239 } 3240 } 3241 } 3242 } elseif ($ClassFormat == 2) { 3243 $tableCount = $this->read_ushort(); 3244 for ($i = 0; $i < $tableCount; $i++) { 3245 $startGlyphID = $this->read_ushort(); 3246 $endGlyphID = $this->read_ushort(); 3247 $class = $this->read_ushort(); 3248 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) { 3249 if ($this->glyphToChar[$g][0]) { 3250 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]); 3251 } 3252 } 3253 } 3254 } 3255 $gbc = []; 3256 foreach ($GlyphByClass as $class => $garr) { 3257 $gbc[$class] = implode('|', $garr); 3258 } 3259 3260 return $gbc; 3261 } 3262 3263 function _getGPOStables() 3264 { 3265 /////////////////////////////////// 3266 // GPOS - Glyph Positioning 3267 /////////////////////////////////// 3268 if (!isset($this->tables["GPOS"])) { 3269 return [[], [], []]; 3270 } 3271 3272 $ffeats = []; 3273 $gpos_offset = $this->seek_table("GPOS"); 3274 $this->skip(4); 3275 $ScriptList_offset = $gpos_offset + $this->read_ushort(); 3276 $FeatureList_offset = $gpos_offset + $this->read_ushort(); 3277 $LookupList_offset = $gpos_offset + $this->read_ushort(); 3278 3279 // ScriptList 3280 $this->seek($ScriptList_offset); 3281 $ScriptCount = $this->read_ushort(); 3282 for ($i = 0; $i < $ScriptCount; $i++) { 3283 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc. 3284 $ScriptTableOffset = $this->read_ushort(); 3285 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset; 3286 } 3287 3288 // Script Table 3289 foreach ($ffeats as $t => $o) { 3290 $ls = []; 3291 $this->seek($o); 3292 $DefLangSys_offset = $this->read_ushort(); 3293 if ($DefLangSys_offset > 0) { 3294 $ls['DFLT'] = $DefLangSys_offset + $o; 3295 } 3296 $LangSysCount = $this->read_ushort(); 3297 for ($i = 0; $i < $LangSysCount; $i++) { 3298 $LangTag = $this->read_tag(); // = 3299 $LangTableOffset = $this->read_ushort(); 3300 $ls[$LangTag] = $o + $LangTableOffset; 3301 } 3302 $ffeats[$t] = $ls; 3303 } 3304 3305 // Get FeatureIndexList 3306 // LangSys Table - from first listed langsys 3307 foreach ($ffeats as $st => $scripts) { 3308 foreach ($scripts as $t => $o) { 3309 $FeatureIndex = []; 3310 $langsyst