composer package updates
[openemr.git] / vendor / mpdf / mpdf / src / OtlDump.php
blob024c54d7db1820d751c322cd4b122822007dbe02
1 <?php
3 namespace Mpdf;
5 // Define the value used in the "head" table of a created TTF file
6 // 0x74727565 "true" for Mac
7 // 0x00010000 for Windows
8 // Either seems to work for a font embedded in a PDF file
9 // when read by Adobe Reader on a Windows PC(!)
10 use Mpdf\Fonts\GlyphOperator;
12 if (!defined('_TTF_MAC_HEADER')) {
13 define("_TTF_MAC_HEADER", false);
16 // Recalculate correct metadata/profiles when making subset fonts (not SIP/SMP)
17 // e.g. xMin, xMax, maxNContours
18 if (!defined('_RECALC_PROFILE')) {
19 define("_RECALC_PROFILE", false);
22 // mPDF 5.7.1
23 if (!function_exists('Mpdf\unicode_hex')) {
24 function unicode_hex($unicode_dec)
26 return (sprintf("%05s", strtoupper(dechex($unicode_dec))));
30 class OtlDump
33 var $GPOSFeatures; // mPDF 5.7.1
35 var $GPOSLookups; // mPDF 5.7.1
37 var $GPOSScriptLang; // mPDF 5.7.1
39 var $ignoreStrings; // mPDF 5.7.1
41 var $MarkAttachmentType; // mPDF 5.7.1
43 var $MarkGlyphSets; // mPDF 7.5.1
45 var $GlyphClassMarks; // mPDF 5.7.1
47 var $GlyphClassLigatures; // mPDF 5.7.1
49 var $GlyphClassBases; // mPDF 5.7.1
51 var $GlyphClassComponents; // mPDF 5.7.1
53 var $GSUBScriptLang; // mPDF 5.7.1
55 var $rtlPUAstr; // mPDF 5.7.1
57 var $rtlPUAarr; // mPDF 5.7.1
59 var $fontkey; // mPDF 5.7.1
61 var $useOTL; // mPDF 5.7.1
63 var $panose;
65 var $maxUni;
67 var $sFamilyClass;
69 var $sFamilySubClass;
71 var $sipset;
73 var $smpset;
75 var $_pos;
77 var $numTables;
79 var $searchRange;
81 var $entrySelector;
83 var $rangeShift;
85 var $tables;
87 var $otables;
89 var $filename;
91 var $fh;
93 var $glyphPos;
95 var $charToGlyph;
97 var $ascent;
99 var $descent;
101 var $name;
103 var $familyName;
105 var $styleName;
107 var $fullName;
109 var $uniqueFontID;
111 var $unitsPerEm;
113 var $bbox;
115 var $capHeight;
117 var $stemV;
119 var $italicAngle;
121 var $flags;
123 var $underlinePosition;
125 var $underlineThickness;
127 var $charWidths;
129 var $defaultWidth;
131 var $maxStrLenRead;
133 var $numTTCFonts;
135 var $TTCFonts;
137 var $maxUniChar;
139 var $kerninfo;
141 var $mode;
143 var $glyphToChar;
145 var $fontRevision;
147 var $glyphdata;
149 var $glyphIDtoUn;
151 var $restrictedUse;
153 var $GSUBFeatures;
155 var $GSUBLookups;
157 var $glyphIDtoUni;
159 var $GSLuCoverage;
161 var $version;
163 private $mpdf;
165 public function __construct(Mpdf $mpdf)
167 $this->mpdf = $mpdf;
168 $this->maxStrLenRead = 200000; // Maximum size of glyf table to read in as string (otherwise reads each glyph from file)
171 function getMetrics($file, $fontkey, $TTCfontID = 0, $debug = false, $BMPonly = false, $kerninfo = false, $useOTL = 0, $mode = null)
173 // mPDF 5.7.1
174 $this->mode = $mode;
175 $this->useOTL = $useOTL; // mPDF 5.7.1
176 $this->fontkey = $fontkey; // mPDF 5.7.1
177 $this->filename = $file;
178 $this->fh = fopen($file, 'rb');
180 if (!$this->fh) {
181 throw new \Mpdf\MpdfException(sprintf('Unable to open file "%s"', $file));
184 $this->_pos = 0;
185 $this->charWidths = '';
186 $this->glyphPos = [];
187 $this->charToGlyph = [];
188 $this->tables = [];
189 $this->otables = [];
190 $this->kerninfo = [];
191 $this->ascent = 0;
192 $this->descent = 0;
193 $this->numTTCFonts = 0;
194 $this->TTCFonts = [];
195 $this->version = $version = $this->read_ulong();
196 $this->panose = [];
198 if ($version == 0x4F54544F) {
199 throw new \Mpdf\MpdfException("Postscript outlines are not supported");
202 if ($version == 0x74746366 && !$TTCfontID) {
203 throw new \Mpdf\MpdfException("TTCfontID for a TrueType Collection has to be defined in ttfontdata configuration key (" . $file . ")");
206 if (!in_array($version, [0x00010000, 0x74727565]) && !$TTCfontID) {
207 throw new \Mpdf\MpdfException("Not a TrueType font: version=" . $version);
210 if ($TTCfontID > 0) {
211 $this->version = $version = $this->read_ulong(); // TTC Header version now
212 if (!in_array($version, [0x00010000, 0x00020000])) {
213 throw new \Mpdf\MpdfException("Error parsing TrueType Collection: version=" . $version . " - " . $file);
215 $this->numTTCFonts = $this->read_ulong();
216 for ($i = 1; $i <= $this->numTTCFonts; $i++) {
217 $this->TTCFonts[$i]['offset'] = $this->read_ulong();
219 $this->seek($this->TTCFonts[$TTCfontID]['offset']);
220 $this->version = $version = $this->read_ulong(); // TTFont version again now
222 $this->readTableDirectory($debug);
223 $this->extractInfo($debug, $BMPonly, $kerninfo, $useOTL);
224 fclose($this->fh);
227 function readTableDirectory($debug = false)
229 $this->numTables = $this->read_ushort();
230 $this->searchRange = $this->read_ushort();
231 $this->entrySelector = $this->read_ushort();
232 $this->rangeShift = $this->read_ushort();
233 $this->tables = [];
234 for ($i = 0; $i < $this->numTables; $i++) {
235 $record = [];
236 $record['tag'] = $this->read_tag();
237 $record['checksum'] = [$this->read_ushort(), $this->read_ushort()];
238 $record['offset'] = $this->read_ulong();
239 $record['length'] = $this->read_ulong();
240 $this->tables[$record['tag']] = $record;
242 if ($debug) {
243 $this->checksumTables();
247 function checksumTables()
249 // Check the checksums for all tables
250 foreach ($this->tables as $t) {
251 if ($t['length'] > 0 && $t['length'] < $this->maxStrLenRead) { // 1.02
252 $table = $this->get_chunk($t['offset'], $t['length']);
253 $checksum = $this->calcChecksum($table);
254 if ($t['tag'] == 'head') {
255 $up = unpack('n*', substr($table, 8, 4));
256 $adjustment[0] = $up[1];
257 $adjustment[1] = $up[2];
258 $checksum = $this->sub32($checksum, $adjustment);
260 $xchecksum = $t['checksum'];
261 if ($xchecksum != $checksum) {
262 throw new \Mpdf\MpdfException(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])));
268 function sub32($x, $y)
270 $xlo = $x[1];
271 $xhi = $x[0];
272 $ylo = $y[1];
273 $yhi = $y[0];
274 if ($ylo > $xlo) {
275 $xlo += 1 << 16;
276 $yhi += 1;
278 $reslo = $xlo - $ylo;
279 if ($yhi > $xhi) {
280 $xhi += 1 << 16;
282 $reshi = $xhi - $yhi;
283 $reshi = $reshi & 0xFFFF;
285 return [$reshi, $reslo];
288 function calcChecksum($data)
290 if (strlen($data) % 4) {
291 $data .= str_repeat("\0", (4 - (strlen($data) % 4)));
293 $len = strlen($data);
294 $hi = 0x0000;
295 $lo = 0x0000;
296 for ($i = 0; $i < $len; $i += 4) {
297 $hi += (ord($data[$i]) << 8) + ord($data[$i + 1]);
298 $lo += (ord($data[$i + 2]) << 8) + ord($data[$i + 3]);
299 $hi += ($lo >> 16) & 0xFFFF;
300 $lo = $lo & 0xFFFF;
303 return [$hi, $lo];
306 function get_table_pos($tag)
308 $offset = isset($this->tables[$tag]['offset']) ? $this->tables[$tag]['offset'] : null;
309 $length = isset($this->tables[$tag]['length']) ? $this->tables[$tag]['length'] : null;
311 return [$offset, $length];
314 function seek($pos)
316 $this->_pos = $pos;
317 fseek($this->fh, $this->_pos);
320 function skip($delta)
322 $this->_pos = $this->_pos + $delta;
323 fseek($this->fh, $delta, SEEK_CUR);
326 function seek_table($tag, $offset_in_table = 0)
328 $tpos = $this->get_table_pos($tag);
329 $this->_pos = $tpos[0] + $offset_in_table;
330 fseek($this->fh, $this->_pos);
332 return $this->_pos;
335 function read_tag()
337 $this->_pos += 4;
339 return fread($this->fh, 4);
342 function read_short()
344 $this->_pos += 2;
345 $s = fread($this->fh, 2);
346 $a = (ord($s[0]) << 8) + ord($s[1]);
347 if ($a & (1 << 15)) {
348 $a = ($a - (1 << 16));
351 return $a;
354 function unpack_short($s)
356 $a = (ord($s[0]) << 8) + ord($s[1]);
357 if ($a & (1 << 15)) {
358 $a = ($a - (1 << 16));
361 return $a;
364 function read_ushort()
366 $this->_pos += 2;
367 $s = fread($this->fh, 2);
369 return (ord($s[0]) << 8) + ord($s[1]);
372 function read_ulong()
374 $this->_pos += 4;
375 $s = fread($this->fh, 4);
377 // if large uInt32 as an integer, PHP converts it to -ve
378 return (ord($s[0]) * 16777216) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); // 16777216 = 1<<24
381 function get_ushort($pos)
383 fseek($this->fh, $pos);
384 $s = fread($this->fh, 2);
386 return (ord($s[0]) << 8) + ord($s[1]);
389 function get_ulong($pos)
391 fseek($this->fh, $pos);
392 $s = fread($this->fh, 4);
394 // iF large uInt32 as an integer, PHP converts it to -ve
395 return (ord($s[0]) * 16777216) + (ord($s[1]) << 16) + (ord($s[2]) << 8) + ord($s[3]); // 16777216 = 1<<24
398 function pack_short($val)
400 if ($val < 0) {
401 $val = abs($val);
402 $val = ~$val;
403 $val += 1;
406 return pack("n", $val);
409 function splice($stream, $offset, $value)
411 return substr($stream, 0, $offset) . $value . substr($stream, $offset + strlen($value));
414 function _set_ushort($stream, $offset, $value)
416 $up = pack("n", $value);
418 return $this->splice($stream, $offset, $up);
421 function _set_short($stream, $offset, $val)
423 if ($val < 0) {
424 $val = abs($val);
425 $val = ~$val;
426 $val += 1;
428 $up = pack("n", $val);
430 return $this->splice($stream, $offset, $up);
433 function get_chunk($pos, $length)
435 fseek($this->fh, $pos);
436 if ($length < 1) {
437 return '';
440 return (fread($this->fh, $length));
443 function get_table($tag)
445 list($pos, $length) = $this->get_table_pos($tag);
446 if ($length == 0) {
447 return '';
449 fseek($this->fh, $pos);
451 return (fread($this->fh, $length));
454 function add($tag, $data)
456 if ($tag == 'head') {
457 $data = $this->splice($data, 8, "\0\0\0\0");
459 $this->otables[$tag] = $data;
462 /////////////////////////////////////////////////////////////////////////////////////////
463 /////////////////////////////////////////////////////////////////////////////////////////
465 function extractInfo($debug = false, $BMPonly = false, $kerninfo = false, $useOTL = 0)
467 $this->panose = [];
468 $this->sFamilyClass = 0;
469 $this->sFamilySubClass = 0;
470 ///////////////////////////////////
471 // name - Naming table
472 ///////////////////////////////////
473 $name_offset = $this->seek_table("name");
474 $format = $this->read_ushort();
475 if ($format != 0 && $format != 1) {
476 throw new \Mpdf\MpdfException("Unknown name table format " . $format);
478 $numRecords = $this->read_ushort();
479 $string_data_offset = $name_offset + $this->read_ushort();
480 $names = [1 => '', 2 => '', 3 => '', 4 => '', 6 => ''];
481 $K = array_keys($names);
482 $nameCount = count($names);
483 for ($i = 0; $i < $numRecords; $i++) {
484 $platformId = $this->read_ushort();
485 $encodingId = $this->read_ushort();
486 $languageId = $this->read_ushort();
487 $nameId = $this->read_ushort();
488 $length = $this->read_ushort();
489 $offset = $this->read_ushort();
490 if (!in_array($nameId, $K)) {
491 continue;
493 $N = '';
494 if ($platformId == 3 && $encodingId == 1 && $languageId == 0x409) { // Microsoft, Unicode, US English, PS Name
495 $opos = $this->_pos;
496 $this->seek($string_data_offset + $offset);
497 if ($length % 2 != 0) {
498 throw new \Mpdf\MpdfException("PostScript name is UTF-16BE string of odd length");
500 $length /= 2;
501 $N = '';
502 while ($length > 0) {
503 $char = $this->read_ushort();
504 $N .= (chr($char));
505 $length -= 1;
507 $this->_pos = $opos;
508 $this->seek($opos);
509 } else {
510 if ($platformId == 1 && $encodingId == 0 && $languageId == 0) { // Macintosh, Roman, English, PS Name
511 $opos = $this->_pos;
512 $N = $this->get_chunk($string_data_offset + $offset, $length);
513 $this->_pos = $opos;
514 $this->seek($opos);
517 if ($N && $names[$nameId] == '') {
518 $names[$nameId] = $N;
519 $nameCount -= 1;
520 if ($nameCount == 0) {
521 break;
525 if ($names[6]) {
526 $psName = $names[6];
527 } else {
528 if ($names[4]) {
529 $psName = preg_replace('/ /', '-', $names[4]);
530 } else {
531 if ($names[1]) {
532 $psName = preg_replace('/ /', '-', $names[1]);
533 } else {
534 $psName = '';
538 if (!$psName) {
539 throw new \Mpdf\MpdfException("Could not find PostScript font name: " . $this->filename);
541 if ($debug) {
542 for ($i = 0; $i < count($psName); $i++) {
543 $c = $psName[$i];
544 $oc = ord($c);
545 if ($oc > 126 || strpos(' [](){}<>/%', $c) !== false) {
546 throw new \Mpdf\MpdfException("psName=" . $psName . " contains invalid character " . $c . " ie U+" . ord(c));
550 $this->name = $psName;
551 if ($names[1]) {
552 $this->familyName = $names[1];
553 } else {
554 $this->familyName = $psName;
556 if ($names[2]) {
557 $this->styleName = $names[2];
558 } else {
559 $this->styleName = 'Regular';
561 if ($names[4]) {
562 $this->fullName = $names[4];
563 } else {
564 $this->fullName = $psName;
566 if ($names[3]) {
567 $this->uniqueFontID = $names[3];
568 } else {
569 $this->uniqueFontID = $psName;
572 if ($names[6]) {
573 $this->fullName = $names[6];
576 ///////////////////////////////////
577 // head - Font header table
578 ///////////////////////////////////
579 $this->seek_table("head");
580 if ($debug) {
581 $ver_maj = $this->read_ushort();
582 $ver_min = $this->read_ushort();
583 if ($ver_maj != 1) {
584 throw new \Mpdf\MpdfException('Unknown head table version ' . $ver_maj . '.' . $ver_min);
586 $this->fontRevision = $this->read_ushort() . $this->read_ushort();
588 $this->skip(4);
589 $magic = $this->read_ulong();
590 if ($magic != 0x5F0F3CF5) {
591 throw new \Mpdf\MpdfException('Invalid head table magic ' . $magic);
593 $this->skip(2);
594 } else {
595 $this->skip(18);
597 $this->unitsPerEm = $unitsPerEm = $this->read_ushort();
598 $scale = 1000 / $unitsPerEm;
599 $this->skip(16);
600 $xMin = $this->read_short();
601 $yMin = $this->read_short();
602 $xMax = $this->read_short();
603 $yMax = $this->read_short();
604 $this->bbox = [($xMin * $scale), ($yMin * $scale), ($xMax * $scale), ($yMax * $scale)];
605 $this->skip(3 * 2);
606 $indexToLocFormat = $this->read_ushort();
607 $glyphDataFormat = $this->read_ushort();
608 if ($glyphDataFormat != 0) {
609 throw new \Mpdf\MpdfException('Unknown glyph data format ' . $glyphDataFormat);
612 ///////////////////////////////////
613 // hhea metrics table
614 ///////////////////////////////////
615 // ttf2t1 seems to use this value rather than the one in OS/2 - so put in for compatibility
616 if (isset($this->tables["hhea"])) {
617 $this->seek_table("hhea");
618 $this->skip(4);
619 $hheaAscender = $this->read_short();
620 $hheaDescender = $this->read_short();
621 $this->ascent = ($hheaAscender * $scale);
622 $this->descent = ($hheaDescender * $scale);
625 ///////////////////////////////////
626 // OS/2 - OS/2 and Windows metrics table
627 ///////////////////////////////////
628 if (isset($this->tables["OS/2"])) {
629 $this->seek_table("OS/2");
630 $version = $this->read_ushort();
631 $this->skip(2);
632 $usWeightClass = $this->read_ushort();
633 $this->skip(2);
634 $fsType = $this->read_ushort();
635 if ($fsType == 0x0002 || ($fsType & 0x0300) != 0) {
636 global $overrideTTFFontRestriction;
637 if (!$overrideTTFFontRestriction) {
638 throw new \Mpdf\MpdfException('ERROR - Font file ' . $this->filename . ' cannot be embedded due to copyright restrictions.');
640 $this->restrictedUse = true;
642 $this->skip(20);
643 $sF = $this->read_short();
644 $this->sFamilyClass = ($sF >> 8);
645 $this->sFamilySubClass = ($sF & 0xFF);
646 $this->_pos += 10; //PANOSE = 10 byte length
647 $panose = fread($this->fh, 10);
648 $this->panose = [];
649 for ($p = 0; $p < strlen($panose); $p++) {
650 $this->panose[] = ord($panose[$p]);
652 $this->skip(26);
653 $sTypoAscender = $this->read_short();
654 $sTypoDescender = $this->read_short();
655 if (!$this->ascent) {
656 $this->ascent = ($sTypoAscender * $scale);
658 if (!$this->descent) {
659 $this->descent = ($sTypoDescender * $scale);
661 if ($version > 1) {
662 $this->skip(16);
663 $sCapHeight = $this->read_short();
664 $this->capHeight = ($sCapHeight * $scale);
665 } else {
666 $this->capHeight = $this->ascent;
668 } else {
669 $usWeightClass = 500;
670 if (!$this->ascent) {
671 $this->ascent = ($yMax * $scale);
673 if (!$this->descent) {
674 $this->descent = ($yMin * $scale);
676 $this->capHeight = $this->ascent;
678 $this->stemV = 50 + intval(pow(($usWeightClass / 65.0), 2));
680 ///////////////////////////////////
681 // post - PostScript table
682 ///////////////////////////////////
683 $this->seek_table("post");
684 if ($debug) {
685 $ver_maj = $this->read_ushort();
686 $ver_min = $this->read_ushort();
687 if ($ver_maj < 1 || $ver_maj > 4) {
688 throw new \Mpdf\MpdfException('Unknown post table version ' . $ver_maj);
690 } else {
691 $this->skip(4);
693 $this->italicAngle = $this->read_short() + $this->read_ushort() / 65536.0;
694 $this->underlinePosition = $this->read_short() * $scale;
695 $this->underlineThickness = $this->read_short() * $scale;
696 $isFixedPitch = $this->read_ulong();
698 $this->flags = 4;
700 if ($this->italicAngle != 0) {
701 $this->flags = $this->flags | 64;
703 if ($usWeightClass >= 600) {
704 $this->flags = $this->flags | 262144;
706 if ($isFixedPitch) {
707 $this->flags = $this->flags | 1;
710 ///////////////////////////////////
711 // hhea - Horizontal header table
712 ///////////////////////////////////
713 $this->seek_table("hhea");
714 if ($debug) {
715 $ver_maj = $this->read_ushort();
716 $ver_min = $this->read_ushort();
717 if ($ver_maj != 1) {
718 throw new \Mpdf\MpdfException('Unknown hhea table version ' . $ver_maj);
720 $this->skip(28);
721 } else {
722 $this->skip(32);
724 $metricDataFormat = $this->read_ushort();
725 if ($metricDataFormat != 0) {
726 throw new \Mpdf\MpdfException('Unknown horizontal metric data format ' . $metricDataFormat);
728 $numberOfHMetrics = $this->read_ushort();
729 if ($numberOfHMetrics == 0) {
730 throw new \Mpdf\MpdfException('Number of horizontal metrics is 0');
733 ///////////////////////////////////
734 // maxp - Maximum profile table
735 ///////////////////////////////////
736 $this->seek_table("maxp");
737 if ($debug) {
738 $ver_maj = $this->read_ushort();
739 $ver_min = $this->read_ushort();
740 if ($ver_maj != 1) {
741 throw new \Mpdf\MpdfException('Unknown maxp table version ' . $ver_maj);
743 } else {
744 $this->skip(4);
746 $numGlyphs = $this->read_ushort();
748 ///////////////////////////////////
749 // cmap - Character to glyph index mapping table
750 ///////////////////////////////////
751 $cmap_offset = $this->seek_table("cmap");
752 $this->skip(2);
753 $cmapTableCount = $this->read_ushort();
754 $unicode_cmap_offset = 0;
755 for ($i = 0; $i < $cmapTableCount; $i++) {
756 $platformID = $this->read_ushort();
757 $encodingID = $this->read_ushort();
758 $offset = $this->read_ulong();
759 $save_pos = $this->_pos;
760 if (($platformID == 3 && $encodingID == 1) || $platformID == 0) { // Microsoft, Unicode
761 $format = $this->get_ushort($cmap_offset + $offset);
762 if ($format == 4) {
763 if (!$unicode_cmap_offset) {
764 $unicode_cmap_offset = $cmap_offset + $offset;
766 if ($BMPonly) {
767 break;
770 } // Microsoft, Unicode Format 12 table HKCS
771 else {
772 if ((($platformID == 3 && $encodingID == 10) || $platformID == 0) && !$BMPonly) {
773 $format = $this->get_ushort($cmap_offset + $offset);
774 if ($format == 12) {
775 $unicode_cmap_offset = $cmap_offset + $offset;
776 break;
780 $this->seek($save_pos);
783 if (!$unicode_cmap_offset) {
784 throw new \Mpdf\MpdfException('Font (' . $this->filename . ') does not have cmap for Unicode (platform 3, encoding 1, format 4, or platform 0, any encoding, format 4)');
787 $sipset = false;
788 $smpset = false;
790 // mPDF 5.7.1
791 $this->GSUBScriptLang = [];
792 $this->rtlPUAstr = '';
793 $this->rtlPUAarr = [];
794 $this->GSUBFeatures = [];
795 $this->GSUBLookups = [];
796 $this->GPOSScriptLang = [];
797 $this->GPOSFeatures = [];
798 $this->GPOSLookups = [];
799 $this->glyphIDtoUni = '';
801 // Format 12 CMAP does characters above Unicode BMP i.e. some HKCS characters U+20000 and above
802 if ($format == 12 && !$BMPonly) {
803 $this->maxUniChar = 0;
804 $this->seek($unicode_cmap_offset + 4);
805 $length = $this->read_ulong();
806 $limit = $unicode_cmap_offset + $length;
807 $this->skip(4);
809 $nGroups = $this->read_ulong();
811 $glyphToChar = [];
812 $charToGlyph = [];
813 for ($i = 0; $i < $nGroups; $i++) {
814 $startCharCode = $this->read_ulong();
815 $endCharCode = $this->read_ulong();
816 $startGlyphCode = $this->read_ulong();
817 if ($endCharCode > 0x20000 && $endCharCode < 0x2FFFF) {
818 $sipset = true;
819 } else {
820 if ($endCharCode > 0x10000 && $endCharCode < 0x1FFFF) {
821 $smpset = true;
824 $offset = 0;
825 for ($unichar = $startCharCode; $unichar <= $endCharCode; $unichar++) {
826 $glyph = $startGlyphCode + $offset;
827 $offset++;
828 if ($unichar < 0x30000) {
829 $charToGlyph[$unichar] = $glyph;
830 $this->maxUniChar = max($unichar, $this->maxUniChar);
831 $glyphToChar[$glyph][] = $unichar;
835 } else {
836 $glyphToChar = [];
837 $charToGlyph = [];
838 $this->getCMAP4($unicode_cmap_offset, $glyphToChar, $charToGlyph);
840 $this->sipset = $sipset;
841 $this->smpset = $smpset;
843 ///////////////////////////////////
844 // mPDF 5.7.1
845 // Map Unmapped glyphs - from $numGlyphs
846 if ($this->useOTL) {
847 $bctr = 0xE000;
848 for ($gid = 1; $gid < $numGlyphs; $gid++) {
849 if (!isset($glyphToChar[$gid])) {
850 while (isset($charToGlyph[$bctr])) {
851 $bctr++;
852 } // Avoid overwriting a glyph already mapped in PUA
853 if (($bctr > 0xF8FF) && ($bctr < 0x2CEB0)) {
854 if (!$BMPonly) {
855 $bctr = 0x2CEB0; // Use unassigned area 0x2CEB0 to 0x2F7FF (space for 10,000 characters)
856 $this->sipset = $sipset = true; // forces subsetting; also ensure charwidths are saved
857 while (isset($charToGlyph[$bctr])) {
858 $bctr++;
860 } else {
861 throw new \Mpdf\MpdfException($names[1] . " : WARNING - The font does not have enough space to map all (unmapped) included glyphs into Private Use Area U+E000 - U+F8FF");
864 $glyphToChar[$gid][] = $bctr;
865 $charToGlyph[$bctr] = $gid;
866 $this->maxUniChar = max($bctr, $this->maxUniChar);
867 $bctr++;
871 $this->glyphToChar = $glyphToChar;
872 $this->charToGlyph = $charToGlyph;
873 ///////////////////////////////////
874 // mPDF 5.7.1 OpenType Layout tables
875 $this->GSUBScriptLang = [];
876 $this->rtlPUAstr = '';
877 $this->rtlPUAarr = [];
878 if ($useOTL) {
879 $this->_getGDEFtables();
880 list($this->GSUBScriptLang, $this->GSUBFeatures, $this->GSUBLookups, $this->rtlPUAstr, $this->rtlPUAarr) = $this->_getGSUBtables();
881 list($this->GPOSScriptLang, $this->GPOSFeatures, $this->GPOSLookups) = $this->_getGPOStables();
882 $this->glyphIDtoUni = str_pad('', 256 * 256 * 3, "\x00");
883 foreach ($glyphToChar as $gid => $arr) {
884 if (isset($glyphToChar[$gid][0])) {
885 $char = $glyphToChar[$gid][0];
886 if ($char != 0 && $char != 65535) {
887 $this->glyphIDtoUni[$gid * 3] = chr($char >> 16);
888 $this->glyphIDtoUni[$gid * 3 + 1] = chr(($char >> 8) & 0xFF);
889 $this->glyphIDtoUni[$gid * 3 + 2] = chr($char & 0xFF);
894 ///////////////////////////////////
895 ///////////////////////////////////
896 // hmtx - Horizontal metrics table
897 ///////////////////////////////////
898 $this->getHMTX($numberOfHMetrics, $numGlyphs, $glyphToChar, $scale);
900 ///////////////////////////////////
901 // kern - Kerning pair table
902 ///////////////////////////////////
903 if ($kerninfo) {
904 // Recognises old form of Kerning table - as required by Windows - Format 0 only
905 $kern_offset = $this->seek_table("kern");
906 $version = $this->read_ushort();
907 $nTables = $this->read_ushort();
908 // subtable header
909 $sversion = $this->read_ushort();
910 $slength = $this->read_ushort();
911 $scoverage = $this->read_ushort();
912 $format = $scoverage >> 8;
913 if ($kern_offset && $version == 0 && $format == 0) {
914 // Format 0
915 $nPairs = $this->read_ushort();
916 $this->skip(6);
917 for ($i = 0; $i < $nPairs; $i++) {
918 $left = $this->read_ushort();
919 $right = $this->read_ushort();
920 $val = $this->read_short();
921 if (count($glyphToChar[$left]) == 1 && count($glyphToChar[$right]) == 1) {
922 if ($left != 32 && $right != 32) {
923 $this->kerninfo[$glyphToChar[$left][0]][$glyphToChar[$right][0]] = intval($val * $scale);
931 /////////////////////////////////////////////////////////////////////////////////////////
932 function _getGDEFtables()
934 ///////////////////////////////////
935 // GDEF - Glyph Definition
936 ///////////////////////////////////
937 // http://www.microsoft.com/typography/otspec/gdef.htm
938 if (isset($this->tables["GDEF"])) {
939 if ($this->mode == 'summary') {
940 $this->mpdf->WriteHTML('<h1>GDEF table</h1>');
942 $gdef_offset = $this->seek_table("GDEF");
943 // ULONG Version of the GDEF table-currently 0x00010000
944 $ver_maj = $this->read_ushort();
945 $ver_min = $this->read_ushort();
946 // Version 0x00010002 of GDEF header contains additional Offset to a list defining mark glyph set definitions (MarkGlyphSetDef)
947 $GlyphClassDef_offset = $this->read_ushort();
948 $AttachList_offset = $this->read_ushort();
949 $LigCaretList_offset = $this->read_ushort();
950 $MarkAttachClassDef_offset = $this->read_ushort();
951 if ($ver_min == 2) {
952 $MarkGlyphSetsDef_offset = $this->read_ushort();
955 // GlyphClassDef
956 $this->seek($gdef_offset + $GlyphClassDef_offset);
958 1 Base glyph (single character, spacing glyph)
959 2 Ligature glyph (multiple character, spacing glyph)
960 3 Mark glyph (non-spacing combining glyph)
961 4 Component glyph (part of single character, spacing glyph)
963 $GlyphByClass = $this->_getClassDefinitionTable();
965 if ($this->mode == 'summary') {
966 $this->mpdf->WriteHTML('<h2>Glyph classes</h2>');
969 if (isset($GlyphByClass[1]) && count($GlyphByClass[1]) > 0) {
970 $this->GlyphClassBases = $this->formatClassArr($GlyphByClass[1]);
971 if ($this->mode == 'summary') {
972 $this->mpdf->WriteHTML('<h3>Glyph class 1</h3>');
973 $this->mpdf->WriteHTML('<h5>Base glyph (single character, spacing glyph)</h5>');
974 $html = '';
975 $html .= '<div class="glyphs">';
976 foreach ($GlyphByClass[1] as $g) {
977 $html .= '&#x' . $g . '; ';
979 $html .= '</div>';
980 $this->mpdf->WriteHTML($html);
982 } else {
983 $this->GlyphClassBases = '';
985 if (isset($GlyphByClass[2]) && count($GlyphByClass[2]) > 0) {
986 $this->GlyphClassLigatures = $this->formatClassArr($GlyphByClass[2]);
987 if ($this->mode == 'summary') {
988 $this->mpdf->WriteHTML('<h3>Glyph class 2</h3>');
989 $this->mpdf->WriteHTML('<h5>Ligature glyph (multiple character, spacing glyph)</h5>');
990 $html = '';
991 $html .= '<div class="glyphs">';
992 foreach ($GlyphByClass[2] as $g) {
993 $html .= '&#x' . $g . '; ';
995 $html .= '</div>';
996 $this->mpdf->WriteHTML($html);
998 } else {
999 $this->GlyphClassLigatures = '';
1001 if (isset($GlyphByClass[3]) && count($GlyphByClass[3]) > 0) {
1002 $this->GlyphClassMarks = $this->formatClassArr($GlyphByClass[3]);
1003 if ($this->mode == 'summary') {
1004 $this->mpdf->WriteHTML('<h3>Glyph class 3</h3>');
1005 $this->mpdf->WriteHTML('<h5>Mark glyph (non-spacing combining glyph)</h5>');
1006 $html = '';
1007 $html .= '<div class="glyphs">';
1008 foreach ($GlyphByClass[3] as $g) {
1009 $html .= '&#x25cc;&#x' . $g . '; ';
1011 $html .= '</div>';
1012 $this->mpdf->WriteHTML($html);
1014 } else {
1015 $this->GlyphClassMarks = '';
1017 if (isset($GlyphByClass[4]) && count($GlyphByClass[4]) > 0) {
1018 $this->GlyphClassComponents = $this->formatClassArr($GlyphByClass[4]);
1019 if ($this->mode == 'summary') {
1020 $this->mpdf->WriteHTML('<h3>Glyph class 4</h3>');
1021 $this->mpdf->WriteHTML('<h5>Component glyph (part of single character, spacing glyph)</h5>');
1022 $html = '';
1023 $html .= '<div class="glyphs">';
1024 foreach ($GlyphByClass[4] as $g) {
1025 $html .= '&#x' . $g . '; ';
1027 $html .= '</div>';
1028 $this->mpdf->WriteHTML($html);
1030 } else {
1031 $this->GlyphClassComponents = '';
1034 $Marks = $GlyphByClass[3]; // to use for MarkAttachmentType
1036 /* Required for GPOS
1037 // Attachment List
1038 if ($AttachList_offset) {
1039 $this->seek($gdef_offset+$AttachList_offset );
1041 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.
1043 The Attachment List table (AttachList) may be used to cache attachment point coordinates along with glyph bitmaps.
1045 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.
1046 AttachList table
1047 Type Name Description
1048 Offset Coverage Offset to Coverage table - from beginning of AttachList table
1049 uint16 GlyphCount Number of glyphs with attachment points
1050 Offset AttachPoint[GlyphCount] Array of offsets to AttachPoint tables-from beginning of AttachList table-in Coverage Index order
1052 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.
1054 AttachPoint table
1055 Type Name Description
1056 uint16 PointCount Number of attachment points on this glyph
1057 uint16 PointIndex[PointCount] Array of contour point indices -in increasing numerical order
1059 See Example 3 - http://www.microsoft.com/typography/otspec/gdef.htm
1062 // Ligature Caret List
1063 // The Ligature Caret List table (LigCaretList) defines caret positions for all the ligatures in a font.
1064 // Not required for mDPF
1065 // MarkAttachmentType
1066 if ($MarkAttachClassDef_offset) {
1067 if ($this->mode == 'summary') {
1068 $this->mpdf->WriteHTML('<h1>Mark Attachment Types</h1>');
1070 $this->seek($gdef_offset + $MarkAttachClassDef_offset);
1071 $MarkAttachmentTypes = $this->_getClassDefinitionTable();
1072 foreach ($MarkAttachmentTypes as $class => $glyphs) {
1073 if (is_array($Marks) && count($Marks)) {
1074 $mat = array_diff($Marks, $MarkAttachmentTypes[$class]);
1075 sort($mat, SORT_STRING);
1076 } else {
1077 $mat = [];
1080 $this->MarkAttachmentType[$class] = $this->formatClassArr($mat);
1082 if ($this->mode == 'summary') {
1083 $this->mpdf->WriteHTML('<h3>Mark Attachment Type: ' . $class . '</h3>');
1084 $html = '';
1085 $html .= '<div class="glyphs">';
1086 foreach ($glyphs as $g) {
1087 $html .= '&#x25cc;&#x' . $g . '; ';
1089 $html .= '</div>';
1090 $this->mpdf->WriteHTML($html);
1093 } else {
1094 $this->MarkAttachmentType = [];
1097 // MarkGlyphSets only in Version 0x00010002 of GDEF
1098 if ($ver_min == 2 && $MarkGlyphSetsDef_offset) {
1099 if ($this->mode == 'summary') {
1100 $this->mpdf->WriteHTML('<h1>Mark Glyph Sets</h1>');
1102 $this->seek($gdef_offset + $MarkGlyphSetsDef_offset);
1103 $MarkSetTableFormat = $this->read_ushort();
1104 $MarkSetCount = $this->read_ushort();
1105 $MarkSetOffset = [];
1106 for ($i = 0; $i < $MarkSetCount; $i++) {
1107 $MarkSetOffset[] = $this->read_ulong();
1109 for ($i = 0; $i < $MarkSetCount; $i++) {
1110 $this->seek($MarkSetOffset[$i]);
1111 $glyphs = $this->_getCoverage();
1112 $this->MarkGlyphSets[$i] = $this->formatClassArr($glyphs);
1113 if ($this->mode == 'summary') {
1114 $this->mpdf->WriteHTML('<h3>Mark Glyph Set class: ' . $i . '</h3>');
1115 $html = '';
1116 $html .= '<div class="glyphs">';
1117 foreach ($glyphs as $g) {
1118 $html .= '&#x25cc;&#x' . $g . '; ';
1120 $html .= '</div>';
1121 $this->mpdf->WriteHTML($html);
1124 } else {
1125 $this->MarkGlyphSets = [];
1127 } else {
1128 $this->mpdf->WriteHTML('<div>GDEF table not defined</div>');
1131 //echo $this->GlyphClassMarks ; exit;
1132 //print_r($GlyphClass); exit;
1133 //print_r($GlyphByClass); exit;
1136 function _getClassDefinitionTable($offset = 0)
1139 if ($offset > 0) {
1140 $this->seek($offset);
1143 // NB Any glyph not included in the range of covered GlyphIDs automatically belongs to Class 0. This is not returned by this function
1144 $ClassFormat = $this->read_ushort();
1145 $GlyphByClass = [];
1146 if ($ClassFormat == 1) {
1147 $StartGlyph = $this->read_ushort();
1148 $GlyphCount = $this->read_ushort();
1149 for ($i = 0; $i < $GlyphCount; $i++) {
1150 $gid = $StartGlyph + $i;
1151 $class = $this->read_ushort();
1152 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]);
1154 } else {
1155 if ($ClassFormat == 2) {
1156 $tableCount = $this->read_ushort();
1157 for ($i = 0; $i < $tableCount; $i++) {
1158 $startGlyphID = $this->read_ushort();
1159 $endGlyphID = $this->read_ushort();
1160 $class = $this->read_ushort();
1161 for ($gid = $startGlyphID; $gid <= $endGlyphID; $gid++) {
1162 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$gid][0]);
1167 ksort($GlyphByClass);
1169 return $GlyphByClass;
1172 function _getGSUBtables()
1174 ///////////////////////////////////
1175 // GSUB - Glyph Substitution
1176 ///////////////////////////////////
1177 if (isset($this->tables["GSUB"])) {
1178 $this->mpdf->WriteHTML('<h1>GSUB Tables</h1>');
1179 $ffeats = [];
1180 $gsub_offset = $this->seek_table("GSUB");
1181 $this->skip(4);
1182 $ScriptList_offset = $gsub_offset + $this->read_ushort();
1183 $FeatureList_offset = $gsub_offset + $this->read_ushort();
1184 $LookupList_offset = $gsub_offset + $this->read_ushort();
1186 // ScriptList
1187 $this->seek($ScriptList_offset);
1188 $ScriptCount = $this->read_ushort();
1189 for ($i = 0; $i < $ScriptCount; $i++) {
1190 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc.
1191 $ScriptTableOffset = $this->read_ushort();
1192 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset;
1195 // Script Table
1196 foreach ($ffeats as $t => $o) {
1197 $ls = [];
1198 $this->seek($o);
1199 $DefLangSys_offset = $this->read_ushort();
1200 if ($DefLangSys_offset > 0) {
1201 $ls['DFLT'] = $DefLangSys_offset + $o;
1203 $LangSysCount = $this->read_ushort();
1204 for ($i = 0; $i < $LangSysCount; $i++) {
1205 $LangTag = $this->read_tag(); // =
1206 $LangTableOffset = $this->read_ushort();
1207 $ls[$LangTag] = $o + $LangTableOffset;
1209 $ffeats[$t] = $ls;
1211 //print_r($ffeats); exit;
1212 // Get FeatureIndexList
1213 // LangSys Table - from first listed langsys
1214 foreach ($ffeats as $st => $scripts) {
1215 foreach ($scripts as $t => $o) {
1216 $FeatureIndex = [];
1217 $langsystable_offset = $o;
1218 $this->seek($langsystable_offset);
1219 $LookUpOrder = $this->read_ushort(); //==NULL
1220 $ReqFeatureIndex = $this->read_ushort();
1221 if ($ReqFeatureIndex != 0xFFFF) {
1222 $FeatureIndex[] = $ReqFeatureIndex;
1224 $FeatureCount = $this->read_ushort();
1225 for ($i = 0; $i < $FeatureCount; $i++) {
1226 $FeatureIndex[] = $this->read_ushort(); // = index of feature
1228 $ffeats[$st][$t] = $FeatureIndex;
1231 //print_r($ffeats); exit;
1232 // Feauture List => LookupListIndex es
1233 $this->seek($FeatureList_offset);
1234 $FeatureCount = $this->read_ushort();
1235 $Feature = [];
1236 for ($i = 0; $i < $FeatureCount; $i++) {
1237 $Feature[$i] = ['tag' => $this->read_tag()];
1238 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort();
1240 for ($i = 0; $i < $FeatureCount; $i++) {
1241 $this->seek($Feature[$i]['offset']);
1242 $this->read_ushort(); // null
1243 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort();
1244 $Feature[$i]['LookupListIndex'] = [];
1245 for ($c = 0; $c < $Lookupcount; $c++) {
1246 $Feature[$i]['LookupListIndex'][] = $this->read_ushort();
1250 foreach ($ffeats as $st => $scripts) {
1251 foreach ($scripts as $t => $o) {
1252 $FeatureIndex = $ffeats[$st][$t];
1253 foreach ($FeatureIndex as $k => $fi) {
1254 $ffeats[$st][$t][$k] = $Feature[$fi];
1258 //=====================================================================================
1259 $gsub = [];
1260 $GSUBScriptLang = [];
1261 foreach ($ffeats as $st => $scripts) {
1262 foreach ($scripts as $t => $langsys) {
1263 $lg = [];
1264 foreach ($langsys as $ft) {
1265 $lg[$ft['LookupListIndex'][0]] = $ft;
1267 // list of Lookups in order they need to be run i.e. order listed in Lookup table
1268 ksort($lg);
1269 foreach ($lg as $ft) {
1270 $gsub[$st][$t][$ft['tag']] = $ft['LookupListIndex'];
1272 if (!isset($GSUBScriptLang[$st])) {
1273 $GSUBScriptLang[$st] = '';
1275 $GSUBScriptLang[$st] .= $t . ' ';
1279 //print_r($gsub); exit;
1281 if ($this->mode == 'summary') {
1282 $this->mpdf->WriteHTML('<h3>GSUB Scripts &amp; Languages</h3>');
1283 $this->mpdf->WriteHTML('<div class="glyphs">');
1284 $html = '';
1285 if (count($gsub)) {
1286 foreach ($gsub as $st => $g) {
1287 $html .= '<h5>' . $st . '</h5>';
1288 foreach ($g as $l => $t) {
1289 $html .= '<div><a href="font_dump_OTL.php?script=' . $st . '&lang=' . $l . '">' . $l . '</a></b>: ';
1290 foreach ($t as $tag => $o) {
1291 $html .= $tag . ' ';
1293 $html .= '</div>';
1296 } else {
1297 $html .= '<div>No entries in GSUB table.</div>';
1299 $this->mpdf->WriteHTML($html);
1300 $this->mpdf->WriteHTML('</div>');
1302 return 0;
1305 //=====================================================================================
1306 // Get metadata and offsets for whole Lookup List table
1307 $this->seek($LookupList_offset);
1308 $LookupCount = $this->read_ushort();
1309 $GSLookup = [];
1310 $Offsets = [];
1311 $SubtableCount = [];
1312 for ($i = 0; $i < $LookupCount; $i++) {
1313 $Offsets[$i] = $LookupList_offset + $this->read_ushort();
1315 for ($i = 0; $i < $LookupCount; $i++) {
1316 $this->seek($Offsets[$i]);
1317 $GSLookup[$i]['Type'] = $this->read_ushort();
1318 $GSLookup[$i]['Flag'] = $flag = $this->read_ushort();
1319 $GSLookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort();
1320 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
1321 $GSLookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort();
1323 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure
1324 if (($flag & 0x0010) == 0x0010) {
1325 $GSLookup[$i]['MarkFilteringSet'] = $this->read_ushort();
1327 // else { $GSLookup[$i]['MarkFilteringSet'] = ''; }
1328 // Lookup Type 7: Extension
1329 if ($GSLookup[$i]['Type'] == 7) {
1330 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type
1331 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
1332 $this->seek($GSLookup[$i]['Subtables'][$c]);
1333 $ExtensionPosFormat = $this->read_ushort();
1334 $type = $this->read_ushort();
1335 $GSLookup[$i]['Subtables'][$c] = $GSLookup[$i]['Subtables'][$c] + $this->read_ulong();
1337 $GSLookup[$i]['Type'] = $type;
1341 //print_r($GSLookup); exit;
1342 //=====================================================================================
1343 // Process Whole LookupList - Get LuCoverage = Lookup coverage just for first glyph
1344 $this->GSLuCoverage = [];
1345 for ($i = 0; $i < $LookupCount; $i++) {
1346 for ($c = 0; $c < $GSLookup[$i]['SubtableCount']; $c++) {
1347 $this->seek($GSLookup[$i]['Subtables'][$c]);
1348 $PosFormat = $this->read_ushort();
1350 if ($GSLookup[$i]['Type'] == 5 && $PosFormat == 3) {
1351 $this->skip(4);
1352 } else {
1353 if ($GSLookup[$i]['Type'] == 6 && $PosFormat == 3) {
1354 $BacktrackGlyphCount = $this->read_ushort();
1355 $this->skip(2 * $BacktrackGlyphCount + 2);
1358 // NB Coverage only looks at glyphs for position 1 (i.e. 5.3 and 6.3) // NEEDS TO READ ALL ********************
1359 $Coverage = $GSLookup[$i]['Subtables'][$c] + $this->read_ushort();
1360 $this->seek($Coverage);
1361 $glyphs = $this->_getCoverage();
1362 $this->GSLuCoverage[$i][$c] = implode('|', $glyphs);
1366 // $this->GSLuCoverage and $GSLookup
1367 //=====================================================================================
1368 $s = '<?php
1369 $GSLuCoverage = ' . var_export($this->GSLuCoverage, true) . ';
1370 ?>';
1372 //=====================================================================================
1373 $s = '<?php
1374 $GlyphClassBases = \'' . $this->GlyphClassBases . '\';
1375 $GlyphClassMarks = \'' . $this->GlyphClassMarks . '\';
1376 $GlyphClassLigatures = \'' . $this->GlyphClassLigatures . '\';
1377 $GlyphClassComponents = \'' . $this->GlyphClassComponents . '\';
1378 $MarkGlyphSets = ' . var_export($this->MarkGlyphSets, true) . ';
1379 $MarkAttachmentType = ' . var_export($this->MarkAttachmentType, true) . ';
1380 ?>';
1382 //=====================================================================================
1383 //=====================================================================================
1384 //=====================================================================================
1385 // Now repeats as original to get Substitution rules
1386 //=====================================================================================
1387 //=====================================================================================
1388 //=====================================================================================
1389 // Get metadata and offsets for whole Lookup List table
1390 $this->seek($LookupList_offset);
1391 $LookupCount = $this->read_ushort();
1392 $Lookup = [];
1393 for ($i = 0; $i < $LookupCount; $i++) {
1394 $Lookup[$i]['offset'] = $LookupList_offset + $this->read_ushort();
1396 for ($i = 0; $i < $LookupCount; $i++) {
1397 $this->seek($Lookup[$i]['offset']);
1398 $Lookup[$i]['Type'] = $this->read_ushort();
1399 $Lookup[$i]['Flag'] = $flag = $this->read_ushort();
1400 $Lookup[$i]['SubtableCount'] = $this->read_ushort();
1401 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1402 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['offset'] + $this->read_ushort();
1404 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure
1405 if (($flag & 0x0010) == 0x0010) {
1406 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort();
1407 } else {
1408 $Lookup[$i]['MarkFilteringSet'] = '';
1411 // Lookup Type 7: Extension
1412 if ($Lookup[$i]['Type'] == 7) {
1413 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type
1414 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1415 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']);
1416 $ExtensionPosFormat = $this->read_ushort();
1417 $type = $this->read_ushort();
1418 $Lookup[$i]['Subtable'][$c]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ulong();
1420 $Lookup[$i]['Type'] = $type;
1424 //print_r($Lookup); exit;
1425 //=====================================================================================
1426 // Process (1) Whole LookupList
1427 for ($i = 0; $i < $LookupCount; $i++) {
1428 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1429 $this->seek($Lookup[$i]['Subtable'][$c]['Offset']);
1430 $SubstFormat = $this->read_ushort();
1431 $Lookup[$i]['Subtable'][$c]['Format'] = $SubstFormat;
1434 Lookup['Type'] Enumeration table for glyph substitution
1435 Value Type Description
1436 1 Single Replace one glyph with one glyph
1437 2 Multiple Replace one glyph with more than one glyph
1438 3 Alternate Replace one glyph with one of many glyphs
1439 4 Ligature Replace multiple glyphs with one glyph
1440 5 Context Replace one or more glyphs in context
1441 6 Chaining Context Replace one or more glyphs in chained context
1442 7 Extension Substitution Extension mechanism for other substitutions (i.e. this excludes the Extension type substitution itself)
1443 8 Reverse chaining context single Applied in reverse order, replace single glyph in chaining context
1446 // LookupType 1: Single Substitution Subtable
1447 if ($Lookup[$i]['Type'] == 1) {
1448 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1449 if ($SubstFormat == 1) { // Calculated output glyph indices
1450 $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'] = $this->read_short();
1451 } else {
1452 if ($SubstFormat == 2) { // Specified output glyph indices
1453 $GlyphCount = $this->read_ushort();
1454 for ($g = 0; $g < $GlyphCount; $g++) {
1455 $Lookup[$i]['Subtable'][$c]['Glyphs'][] = $this->read_ushort();
1459 } // LookupType 2: Multiple Substitution Subtable
1460 else {
1461 if ($Lookup[$i]['Type'] == 2) {
1462 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1463 $Lookup[$i]['Subtable'][$c]['SequenceCount'] = $SequenceCount = $this->read_short();
1464 for ($s = 0; $s < $SequenceCount; $s++) {
1465 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1467 for ($s = 0; $s < $SequenceCount; $s++) {
1468 // Sequence Tables
1469 $this->seek($Lookup[$i]['Subtable'][$c]['Sequences'][$s]['Offset']);
1470 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount'] = $this->read_short();
1471 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['GlyphCount']; $g++) {
1472 $Lookup[$i]['Subtable'][$c]['Sequences'][$s]['SubstituteGlyphID'][] = $this->read_ushort();
1475 } // LookupType 3: Alternate Forms
1476 else {
1477 if ($Lookup[$i]['Type'] == 3) {
1478 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1479 $Lookup[$i]['Subtable'][$c]['AlternateSetCount'] = $AlternateSetCount = $this->read_short();
1480 for ($s = 0; $s < $AlternateSetCount; $s++) {
1481 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1484 for ($s = 0; $s < $AlternateSetCount; $s++) {
1485 // AlternateSet Tables
1486 $this->seek($Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['Offset']);
1487 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount'] = $this->read_short();
1488 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['GlyphCount']; $g++) {
1489 $Lookup[$i]['Subtable'][$c]['AlternateSets'][$s]['SubstituteGlyphID'][] = $this->read_ushort();
1492 } // LookupType 4: Ligature Substitution Subtable
1493 else {
1494 if ($Lookup[$i]['Type'] == 4) {
1495 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1496 $Lookup[$i]['Subtable'][$c]['LigSetCount'] = $LigSetCount = $this->read_short();
1497 for ($s = 0; $s < $LigSetCount; $s++) {
1498 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1500 for ($s = 0; $s < $LigSetCount; $s++) {
1501 // LigatureSet Tables
1502 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset']);
1503 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount'] = $this->read_short();
1504 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
1505 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g] = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Offset'] + $this->read_ushort();
1508 for ($s = 0; $s < $LigSetCount; $s++) {
1509 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
1510 // Ligature tables
1511 $this->seek($Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigatureOffset'][$g]);
1512 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph'] = $this->read_ushort();
1513 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount'] = $this->read_ushort();
1514 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) {
1515 $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l] = $this->read_ushort();
1519 } // LookupType 5: Contextual Substitution Subtable
1520 else {
1521 if ($Lookup[$i]['Type'] == 5) {
1522 // Format 1: Context Substitution
1523 if ($SubstFormat == 1) {
1524 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1525 $Lookup[$i]['Subtable'][$c]['SubRuleSetCount'] = $SubRuleSetCount = $this->read_short();
1526 for ($s = 0; $s < $SubRuleSetCount; $s++) {
1527 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_short();
1529 for ($s = 0; $s < $SubRuleSetCount; $s++) {
1530 // SubRuleSet Tables
1531 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset']);
1532 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount'] = $this->read_short();
1533 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) {
1534 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['Offset'] + $this->read_ushort();
1537 for ($s = 0; $s < $SubRuleSetCount; $s++) {
1538 // SubRule Tables
1539 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $g++) {
1540 // Ligature tables
1541 $this->seek($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleOffset'][$g]);
1543 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount'] = $this->read_ushort();
1544 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount'] = $this->read_ushort();
1545 // "Input"::[GlyphCount - 1]::Array of input GlyphIDs-start with second glyph
1546 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['GlyphCount']; $l++) {
1547 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['Input'][$l] = $this->read_ushort();
1549 // "SubstLookupRecord"::[SubstCount]::Array of SubstLookupRecords-in design order
1550 for ($l = 0; $l < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstCount']; $l++) {
1551 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['SequenceIndex'] = $this->read_ushort();
1552 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$g]['SubstLookupRecord'][$l]['LookupListIndex'] = $this->read_ushort();
1556 } // Format 2: Class-based Context Glyph Substitution
1557 else {
1558 if ($SubstFormat == 2) {
1559 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1560 $Lookup[$i]['Subtable'][$c]['ClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1561 $Lookup[$i]['Subtable'][$c]['SubClassSetCnt'] = $this->read_ushort();
1562 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $b++) {
1563 $offset = $this->read_ushort();
1564 if ($offset == 0x0000) {
1565 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = 0;
1566 } else {
1567 $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset;
1570 } else {
1571 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Lookup[$i]['Type'] . ", Format " . $SubstFormat . " not supported (ttfontsuni.php).");
1574 } // LookupType 6: Chaining Contextual Substitution Subtable
1575 else {
1576 if ($Lookup[$i]['Type'] == 6) {
1577 // Format 1: Simple Chaining Context Glyph Substitution p255
1578 if ($SubstFormat == 1) {
1579 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1580 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount'] = $this->read_ushort();
1581 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $b++) {
1582 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1584 } // Format 2: Class-based Chaining Context Glyph Substitution p257
1585 else {
1586 if ($SubstFormat == 2) {
1587 $Lookup[$i]['Subtable'][$c]['CoverageTableOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1588 $Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1589 $Lookup[$i]['Subtable'][$c]['InputClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1590 $Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset'] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1591 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt'] = $this->read_ushort();
1592 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $b++) {
1593 $offset = $this->read_ushort();
1594 if ($offset == 0x0000) {
1595 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $offset;
1596 } else {
1597 $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $offset;
1600 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259
1601 else {
1602 if ($SubstFormat == 3) {
1603 $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount'] = $this->read_ushort();
1604 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) {
1605 $Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1607 $Lookup[$i]['Subtable'][$c]['InputGlyphCount'] = $this->read_ushort();
1608 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) {
1609 $Lookup[$i]['Subtable'][$c]['CoverageInput'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1611 $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount'] = $this->read_ushort();
1612 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) {
1613 $Lookup[$i]['Subtable'][$c]['CoverageLookahead'][] = $Lookup[$i]['Subtable'][$c]['Offset'] + $this->read_ushort();
1615 $Lookup[$i]['Subtable'][$c]['SubstCount'] = $this->read_ushort();
1616 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) {
1617 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'] = $this->read_ushort();
1618 $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'] = $this->read_ushort();
1620 Substitution Lookup Record
1621 All contextual substitution subtables specify the substitution data in a Substitution Lookup Record (SubstLookupRecord). Each record contains a SequenceIndex, which indicates the position where the substitution will occur in the glyph sequence. In addition, a LookupListIndex identifies the lookup to be applied at the glyph position specified by the SequenceIndex.
1627 } else {
1628 throw new \Mpdf\MpdfException("Lookup Type " . $Lookup[$i]['Type'] . " not supported.");
1637 //print_r($Lookup); exit;
1638 //=====================================================================================
1639 // Process (2) Whole LookupList
1640 // Get Coverage tables and prepare preg_replace
1641 for ($i = 0; $i < $LookupCount; $i++) {
1642 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
1643 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format'];
1645 // LookupType 1: Single Substitution Subtable 1 => 1
1646 if ($Lookup[$i]['Type'] == 1) {
1647 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1648 $glyphs = $this->_getCoverage(false);
1649 for ($g = 0; $g < count($glyphs); $g++) {
1650 $replace = [];
1651 $substitute = [];
1652 $replace[] = unicode_hex($this->glyphToChar[$glyphs[$g]][0]);
1653 // Flag = Ignore
1654 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1655 continue;
1657 if (isset($Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])) { // Format 1
1658 $substitute[] = unicode_hex($this->glyphToChar[($glyphs[$g] + $Lookup[$i]['Subtable'][$c]['DeltaGlyphID'])][0]);
1659 } else { // Format 2
1660 $substitute[] = unicode_hex($this->glyphToChar[($Lookup[$i]['Subtable'][$c]['Glyphs'][$g])][0]);
1662 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute];
1664 } // LookupType 2: Multiple Substitution Subtable 1 => n
1665 else {
1666 if ($Lookup[$i]['Type'] == 2) {
1667 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1668 $glyphs = $this->_getCoverage();
1669 for ($g = 0; $g < count($glyphs); $g++) {
1670 $replace = [];
1671 $substitute = [];
1672 $replace[] = $glyphs[$g];
1673 // Flag = Ignore
1674 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1675 continue;
1677 if (!isset($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) || count($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID']) == 0) {
1678 continue;
1679 } // Illegal for GlyphCount to be 0; either error in font, or something has gone wrong - lets carry on for now!
1680 foreach ($Lookup[$i]['Subtable'][$c]['Sequences'][$g]['SubstituteGlyphID'] as $sub) {
1681 $substitute[] = unicode_hex($this->glyphToChar[$sub][0]);
1683 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute];
1685 } // LookupType 3: Alternate Forms 1 => 1 (only first alternate form is used)
1686 else {
1687 if ($Lookup[$i]['Type'] == 3) {
1688 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1689 $glyphs = $this->_getCoverage();
1690 for ($g = 0; $g < count($glyphs); $g++) {
1691 $replace = [];
1692 $substitute = [];
1693 $replace[] = $glyphs[$g];
1694 // Flag = Ignore
1695 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1696 continue;
1699 for ($gl = 0; $gl < $Lookup[$i]['Subtable'][$c]['AlternateSets'][$g]['GlyphCount']; $gl++) {
1700 $gid = $Lookup[$i]['Subtable'][$c]['AlternateSets'][$g]['SubstituteGlyphID'][$gl];
1701 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]);
1704 //$gid = $Lookup[$i]['Subtable'][$c]['AlternateSets'][$g]['SubstituteGlyphID'][0];
1705 //$substitute[] = unicode_hex($this->glyphToChar[$gid][0]);
1707 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute];
1709 if ($i == 166) {
1710 print_r($Lookup[$i]['Subtable']);
1711 exit;
1713 } // LookupType 4: Ligature Substitution Subtable n => 1
1714 else {
1715 if ($Lookup[$i]['Type'] == 4) {
1716 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1717 $glyphs = $this->_getCoverage();
1718 $LigSetCount = $Lookup[$i]['Subtable'][$c]['LigSetCount'];
1719 for ($s = 0; $s < $LigSetCount; $s++) {
1720 for ($g = 0; $g < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['LigCount']; $g++) {
1721 $replace = [];
1722 $substitute = [];
1723 $replace[] = $glyphs[$s];
1724 // Flag = Ignore
1725 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $replace[0], $Lookup[$i]['MarkFilteringSet'])) {
1726 continue;
1728 for ($l = 1; $l < $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']; $l++) {
1729 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['GlyphID'][$l];
1730 $rpl = unicode_hex($this->glyphToChar[$gid][0]);
1731 // Flag = Ignore
1732 if ($this->_checkGSUBignore($Lookup[$i]['Flag'], $rpl, $Lookup[$i]['MarkFilteringSet'])) {
1733 continue 2;
1735 $replace[] = $rpl;
1737 $gid = $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['LigGlyph'];
1738 $substitute[] = unicode_hex($this->glyphToChar[$gid][0]);
1739 $Lookup[$i]['Subtable'][$c]['subs'][] = ['Replace' => $replace, 'substitute' => $substitute, 'CompCount' => $Lookup[$i]['Subtable'][$c]['LigSet'][$s]['Ligature'][$g]['CompCount']];
1742 } // LookupType 5: Contextual Substitution Subtable
1743 else {
1744 if ($Lookup[$i]['Type'] == 5) {
1745 // Format 1: Context Substitution
1746 if ($SubstFormat == 1) {
1747 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1748 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
1750 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) {
1751 $SubRuleSet = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s];
1752 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph'] = $CoverageGlyphs[$s];
1753 for ($r = 0; $r < $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRuleCount']; $r++) {
1754 $GlyphCount = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['GlyphCount'];
1755 for ($g = 1; $g < $GlyphCount; $g++) {
1756 $glyphID = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['Input'][$g];
1757 $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
1761 } // Format 2: Class-based Context Glyph Substitution
1762 else {
1763 if ($SubstFormat == 2) {
1764 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1765 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
1767 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['ClassDefOffset']);
1768 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses;
1770 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) {
1771 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) {
1772 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s]);
1773 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt'] = $SubClassRuleCnt = $this->read_ushort();
1774 $SubClassRule = [];
1775 for ($b = 0; $b < $SubClassRuleCnt; $b++) {
1776 $SubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] + $this->read_ushort();
1777 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $SubClassRule[$b];
1782 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubClassSetCnt']; $s++) {
1783 $SubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRuleCnt'];
1784 for ($b = 0; $b < $SubClassRuleCnt; $b++) {
1785 if ($Lookup[$i]['Subtable'][$c]['SubClassSetOffset'][$s] > 0) {
1786 $this->seek($Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b]);
1787 $Rule = [];
1788 $Rule['InputGlyphCount'] = $this->read_ushort();
1789 $Rule['SubstCount'] = $this->read_ushort();
1790 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) {
1791 $Rule['Input'][$r] = $this->read_ushort();
1793 for ($r = 0; $r < $Rule['SubstCount']; $r++) {
1794 $Rule['SequenceIndex'][$r] = $this->read_ushort();
1795 $Rule['LookupListIndex'][$r] = $this->read_ushort();
1798 $Lookup[$i]['Subtable'][$c]['SubClassSet'][$s]['SubClassRule'][$b] = $Rule;
1802 } // Format 3: Coverage-based Context Glyph Substitution
1803 else {
1804 if ($SubstFormat == 3) {
1805 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) {
1806 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]);
1807 $glyphs = $this->_getCoverage();
1808 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs);
1810 throw new \Mpdf\MpdfException("Lookup Type 5, SubstFormat 3 not tested. Please report this with the name of font used - " . $this->fontkey);
1814 } // LookupType 6: Chaining Contextual Substitution Subtable
1815 else {
1816 if ($Lookup[$i]['Type'] == 6) {
1817 // Format 1: Simple Chaining Context Glyph Substitution p255
1818 if ($SubstFormat == 1) {
1819 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1820 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
1822 $ChainSubRuleSetCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount'];
1824 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) {
1825 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s]);
1826 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount'] = $this->read_ushort();
1827 for ($r = 0; $r < $ChainSubRuleCnt; $r++) {
1828 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r] = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetOffset'][$s] + $this->read_ushort();
1831 for ($s = 0; $s < $ChainSubRuleSetCnt; $s++) {
1832 $ChainSubRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleCount'];
1833 for ($r = 0; $r < $ChainSubRuleCnt; $r++) {
1834 // ChainSubRule
1835 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRuleOffset'][$r]);
1837 $BacktrackGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphCount'] = $this->read_ushort();
1838 for ($g = 0; $g < $BacktrackGlyphCount; $g++) {
1839 $glyphID = $this->read_ushort();
1840 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['BacktrackGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
1843 $InputGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphCount'] = $this->read_ushort();
1844 for ($g = 1; $g < $InputGlyphCount; $g++) {
1845 $glyphID = $this->read_ushort();
1846 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['InputGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
1849 $LookaheadGlyphCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphCount'] = $this->read_ushort();
1850 for ($g = 0; $g < $LookaheadGlyphCount; $g++) {
1851 $glyphID = $this->read_ushort();
1852 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookaheadGlyphs'][$g] = unicode_hex($this->glyphToChar[$glyphID][0]);
1855 $SubstCount = $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SubstCount'] = $this->read_ushort();
1856 for ($lu = 0; $lu < $SubstCount; $lu++) {
1857 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['SequenceIndex'][$lu] = $this->read_ushort();
1858 $Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'][$r]['LookupListIndex'][$lu] = $this->read_ushort();
1862 } // Format 2: Class-based Chaining Context Glyph Substitution p257
1863 else {
1864 if ($SubstFormat == 2) {
1865 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageTableOffset']);
1866 $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'] = $CoverageGlyphs = $this->_getCoverage();
1868 $BacktrackClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['BacktrackClassDefOffset']);
1869 $Lookup[$i]['Subtable'][$c]['BacktrackClasses'] = $BacktrackClasses;
1871 $InputClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['InputClassDefOffset']);
1872 $Lookup[$i]['Subtable'][$c]['InputClasses'] = $InputClasses;
1874 $LookaheadClasses = $this->_getClasses($Lookup[$i]['Subtable'][$c]['LookaheadClassDefOffset']);
1875 $Lookup[$i]['Subtable'][$c]['LookaheadClasses'] = $LookaheadClasses;
1877 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) {
1878 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) {
1879 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s]);
1880 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'] = $ChainSubClassRuleCnt = $this->read_ushort();
1881 $ChainSubClassRule = [];
1882 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {
1883 $ChainSubClassRule[$b] = $Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] + $this->read_ushort();
1884 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $ChainSubClassRule[$b];
1889 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubClassSetCnt']; $s++) {
1890 $ChainSubClassRuleCnt = $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRuleCnt'];
1891 for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {
1892 if ($Lookup[$i]['Subtable'][$c]['ChainSubClassSetOffset'][$s] > 0) {
1893 $this->seek($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b]);
1894 $Rule = [];
1895 $Rule['BacktrackGlyphCount'] = $this->read_ushort();
1896 for ($r = 0; $r < $Rule['BacktrackGlyphCount']; $r++) {
1897 $Rule['Backtrack'][$r] = $this->read_ushort();
1899 $Rule['InputGlyphCount'] = $this->read_ushort();
1900 for ($r = 1; $r < $Rule['InputGlyphCount']; $r++) {
1901 $Rule['Input'][$r] = $this->read_ushort();
1903 $Rule['LookaheadGlyphCount'] = $this->read_ushort();
1904 for ($r = 0; $r < $Rule['LookaheadGlyphCount']; $r++) {
1905 $Rule['Lookahead'][$r] = $this->read_ushort();
1907 $Rule['SubstCount'] = $this->read_ushort();
1908 for ($r = 0; $r < $Rule['SubstCount']; $r++) {
1909 $Rule['SequenceIndex'][$r] = $this->read_ushort();
1910 $Rule['LookupListIndex'][$r] = $this->read_ushort();
1913 $Lookup[$i]['Subtable'][$c]['ChainSubClassSet'][$s]['ChainSubClassRule'][$b] = $Rule;
1917 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259
1918 else {
1919 if ($SubstFormat == 3) {
1920 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']; $b++) {
1921 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageBacktrack'][$b]);
1922 $glyphs = $this->_getCoverage();
1923 $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'][] = implode("|", $glyphs);
1925 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['InputGlyphCount']; $b++) {
1926 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageInput'][$b]);
1927 $glyphs = $this->_getCoverage();
1928 $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'][] = implode("|", $glyphs);
1929 // Don't use above value as these are ordered numerically not as need to process
1931 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']; $b++) {
1932 $this->seek($Lookup[$i]['Subtable'][$c]['CoverageLookahead'][$b]);
1933 $glyphs = $this->_getCoverage();
1934 $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'][] = implode("|", $glyphs);
1948 //=====================================================================================
1949 //=====================================================================================
1950 //=====================================================================================
1952 $st = $this->mpdf->OTLscript;
1953 $t = $this->mpdf->OTLlang;
1954 $langsys = $gsub[$st][$t];
1956 $lul = []; // array of LookupListIndexes
1957 $tags = []; // corresponding array of feature tags e.g. 'ccmp'
1958 foreach ($langsys as $tag => $ft) {
1959 foreach ($ft as $ll) {
1960 $lul[$ll] = $tag;
1963 ksort($lul); // Order the Lookups in the order they are in the GUSB table, regardless of Feature order
1964 $this->_getGSUBarray($Lookup, $lul, $st);
1965 //print_r($lul); exit;
1968 //print_r($Lookup); exit;
1970 return [$GSUBScriptLang, $gsub, $GSLookup, $rtlPUAstr, $rtlPUAarr];
1973 /////////////////////////////////////////////////////////////////////////////////////////
1974 // GSUB functions
1975 function _getGSUBarray(&$Lookup, &$lul, $scripttag, $level = 1, $coverage = '', $exB = '', $exL = '')
1977 // Process (3) LookupList for specific Script-LangSys
1978 // Generate preg_replace
1979 $html = '';
1980 if ($level == 1) {
1981 $html .= '<bookmark level="0" content="GSUB features">';
1983 foreach ($lul as $i => $tag) {
1984 $html .= '<div class="level' . $level . '">';
1985 $html .= '<h5 class="level' . $level . '">';
1986 if ($level == 1) {
1987 $html .= '<bookmark level="1" content="' . $tag . ' [#' . $i . ']">';
1989 $html .= 'Lookup #' . $i . ' [tag: <span style="color:#000066;">' . $tag . '</span>]</h5>';
1990 $ignore = $this->_getGSUBignoreString($Lookup[$i]['Flag'], $Lookup[$i]['MarkFilteringSet']);
1991 if ($ignore) {
1992 $html .= '<div class="ignore">Ignoring: ' . $ignore . '</div> ';
1995 $Type = $Lookup[$i]['Type'];
1996 $Flag = $Lookup[$i]['Flag'];
1997 if (($Flag & 0x0001) == 1) {
1998 $dir = 'RTL';
1999 } else {
2000 $dir = 'LTR';
2003 for ($c = 0; $c < $Lookup[$i]['SubtableCount']; $c++) {
2004 $html .= '<div class="subtable">Subtable #' . $c;
2005 if ($level == 1) {
2006 $html .= '<bookmark level="2" content="Subtable #' . $c . '">';
2008 $html .= '</div>';
2010 $SubstFormat = $Lookup[$i]['Subtable'][$c]['Format'];
2012 // LookupType 1: Single Substitution Subtable
2013 if ($Lookup[$i]['Type'] == 1) {
2014 $html .= '<div class="lookuptype">LookupType 1: Single Substitution Subtable</div>';
2015 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2016 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2017 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0];
2018 if ($level == 2 && strpos($coverage, $inputGlyphs[0]) === false) {
2019 continue;
2021 $html .= '<div class="substitution">';
2022 $html .= '<span class="unicode">' . $this->formatUni($inputGlyphs[0]) . '&nbsp;</span> ';
2023 if ($level == 2 && $exB) {
2024 $html .= $exB;
2026 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($inputGlyphs[0]) . '</span>';
2027 if ($level == 2 && $exL) {
2028 $html .= $exL;
2030 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
2031 if ($level == 2 && $exB) {
2032 $html .= $exB;
2034 $html .= '<span class="changed">&nbsp;' . $this->formatEntity($substitute) . '</span>';
2035 if ($level == 2 && $exL) {
2036 $html .= $exL;
2038 $html .= '&nbsp; <span class="unicode">' . $this->formatUni($substitute) . '</span> ';
2039 $html .= '</div>';
2041 } // LookupType 2: Multiple Substitution Subtable
2042 else {
2043 if ($Lookup[$i]['Type'] == 2) {
2044 $html .= '<div class="lookuptype">LookupType 2: Multiple Substitution Subtable</div>';
2045 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2046 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2047 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'];
2048 if ($level == 2 && strpos($coverage, $inputGlyphs[0]) === false) {
2049 continue;
2051 $html .= '<div class="substitution">';
2052 $html .= '<span class="unicode">' . $this->formatUni($inputGlyphs[0]) . '&nbsp;</span> ';
2053 if ($level == 2 && $exB) {
2054 $html .= $exB;
2056 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($inputGlyphs[0]) . '</span>';
2057 if ($level == 2 && $exL) {
2058 $html .= $exL;
2060 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
2061 if ($level == 2 && $exB) {
2062 $html .= $exB;
2064 $html .= '<span class="changed">&nbsp;' . $this->formatEntityArr($substitute) . '</span>';
2065 if ($level == 2 && $exL) {
2066 $html .= $exL;
2068 $html .= '&nbsp; <span class="unicode">' . $this->formatUniArr($substitute) . '</span> ';
2069 $html .= '</div>';
2071 } // LookupType 3: Alternate Forms
2072 else {
2073 if ($Lookup[$i]['Type'] == 3) {
2074 $html .= '<div class="lookuptype">LookupType 3: Alternate Forms</div>';
2075 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2076 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2077 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0];
2078 if ($level == 2 && strpos($coverage, $inputGlyphs[0]) === false) {
2079 continue;
2081 $html .= '<div class="substitution">';
2082 $html .= '<span class="unicode">' . $this->formatUni($inputGlyphs[0]) . '&nbsp;</span> ';
2083 if ($level == 2 && $exB) {
2084 $html .= $exB;
2086 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($inputGlyphs[0]) . '</span>';
2087 if ($level == 2 && $exL) {
2088 $html .= $exL;
2090 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
2091 if ($level == 2 && $exB) {
2092 $html .= $exB;
2094 $html .= '<span class="changed">&nbsp;' . $this->formatEntity($substitute) . '</span>';
2095 if ($level == 2 && $exL) {
2096 $html .= $exL;
2098 $html .= '&nbsp; <span class="unicode">' . $this->formatUni($substitute) . '</span> ';
2099 if (count($Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute']) > 1) {
2100 for ($alt = 1; $alt < count($Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute']); $alt++) {
2101 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][$alt];
2102 $html .= '&nbsp; | &nbsp; ALT #' . $alt . ' &nbsp; ';
2103 $html .= '<span class="changed">&nbsp;' . $this->formatEntity($substitute) . '</span>';
2104 $html .= '&nbsp; <span class="unicode">' . $this->formatUni($substitute) . '</span> ';
2107 $html .= '</div>';
2109 } // LookupType 4: Ligature Substitution Subtable
2110 else {
2111 if ($Lookup[$i]['Type'] == 4) {
2112 $html .= '<div class="lookuptype">LookupType 4: Ligature Substitution Subtable</div>';
2113 for ($s = 0; $s < count($Lookup[$i]['Subtable'][$c]['subs']); $s++) {
2114 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['subs'][$s]['Replace'];
2115 $substitute = $Lookup[$i]['Subtable'][$c]['subs'][$s]['substitute'][0];
2116 if ($level == 2 && strpos($coverage, $inputGlyphs[0]) === false) {
2117 continue;
2119 $html .= '<div class="substitution">';
2120 $html .= '<span class="unicode">' . $this->formatUniArr($inputGlyphs) . '&nbsp;</span> ';
2121 if ($level == 2 && $exB) {
2122 $html .= $exB;
2124 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntityArr($inputGlyphs) . '</span>';
2125 if ($level == 2 && $exL) {
2126 $html .= $exL;
2128 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
2129 if ($level == 2 && $exB) {
2130 $html .= $exB;
2132 $html .= '<span class="changed">&nbsp;' . $this->formatEntity($substitute) . '</span>';
2133 if ($level == 2 && $exL) {
2134 $html .= $exL;
2136 $html .= '&nbsp; <span class="unicode">' . $this->formatUni($substitute) . '</span> ';
2137 $html .= '</div>';
2139 } // LookupType 5: Contextual Substitution Subtable
2140 else {
2141 if ($Lookup[$i]['Type'] == 5) {
2142 $html .= '<div class="lookuptype">LookupType 5: Contextual Substitution Subtable</div>';
2143 // Format 1: Context Substitution
2144 if ($SubstFormat == 1) {
2145 $html .= '<div class="lookuptypesub">Format 1: Context Substitution</div>';
2146 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['SubRuleSetCount']; $s++) {
2147 // SubRuleSet
2148 $subRule = [];
2149 $html .= '<div class="rule">Subrule Set: ' . $s . '</div>';
2150 foreach ($Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['SubRule'] as $rctr => $rule) {
2151 // SubRule
2152 $html .= '<div class="rule">SubRule: ' . $rctr . '</div>';
2153 $inputGlyphs = [];
2154 if ($rule['GlyphCount'] > 1) {
2155 $inputGlyphs = $rule['InputGlyphs'];
2157 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['SubRuleSet'][$s]['FirstGlyph'];
2158 ksort($inputGlyphs);
2159 $nInput = count($inputGlyphs);
2161 $exampleI = [];
2162 $html .= '<div class="context">CONTEXT: ';
2163 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
2164 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
2165 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
2167 $html .= '</div>';
2169 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2170 $lup = $rule['SubstLookupRecord'][$b]['LookupListIndex'];
2171 $seqIndex = $rule['SubstLookupRecord'][$b]['SequenceIndex'];
2173 // GENERATE exampleI[<seqIndex] .... exampleI[>seqIndex]
2174 $exB = '';
2175 $exL = '';
2176 if ($seqIndex > 0) {
2177 $exB .= '<span class="inputother">';
2178 for ($ip = 0; $ip < $seqIndex; $ip++) {
2179 $exB .= $this->formatEntity($inputGlyphs[$ip]) . '&#x200d;';
2181 $exB .= '</span>';
2183 if (count($inputGlyphs) > ($seqIndex + 1)) {
2184 $exL .= '<span class="inputother">';
2185 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
2186 $exL .= $this->formatEntity($inputGlyphs[$ip]) . '&#x200d;';
2188 $exL .= '</span>';
2190 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
2192 $lul2 = [$lup => $tag];
2194 // Only apply if the (first) 'Replace' glyph from the
2195 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2196 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
2197 // to level 2 and only apply if first Replace glyph is in this list
2198 $html .= $this->_getGSUBarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
2201 if (count($subRule['rules'])) {
2202 $volt[] = $subRule;
2206 } // Format 2: Class-based Context Glyph Substitution
2207 else {
2208 if ($SubstFormat == 2) {
2209 $html .= '<div class="lookuptypesub">Format 2: Class-based Context Glyph Substitution</div>';
2210 foreach ($Lookup[$i]['Subtable'][$c]['SubClassSet'] as $inputClass => $cscs) {
2211 $html .= '<div class="rule">Input Class: ' . $inputClass . '</div>';
2212 for ($cscrule = 0; $cscrule < $cscs['SubClassRuleCnt']; $cscrule++) {
2213 $html .= '<div class="rule">Rule: ' . $cscrule . '</div>';
2214 $rule = $cscs['SubClassRule'][$cscrule];
2216 $inputGlyphs = [];
2218 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass];
2220 if ($rule['InputGlyphCount'] > 1) {
2221 // NB starts at 1
2222 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) {
2223 $classindex = $rule['Input'][$gcl];
2224 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex];
2228 // Class 0 contains all the glyphs NOT in the other classes
2229 $class0excl = implode('|', $Lookup[$i]['Subtable'][$c]['InputClasses']);
2231 $exampleI = [];
2232 $html .= '<div class="context">CONTEXT: ';
2233 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
2234 if (!$inputGlyphs[$ff]) {
2235 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;[NOT ' . $this->formatEntityStr($class0excl) . ']&nbsp;</span></div>';
2236 $exampleI[] = '[NOT ' . $this->formatEntityFirst($class0excl) . ']';
2237 } else {
2238 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
2239 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
2242 $html .= '</div>';
2244 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2245 $lup = $rule['LookupListIndex'][$b];
2246 $seqIndex = $rule['SequenceIndex'][$b];
2248 // GENERATE exampleI[<seqIndex] .... exampleI[>seqIndex]
2249 $exB = '';
2250 $exL = '';
2252 if ($seqIndex > 0) {
2253 $exB .= '<span class="inputother">';
2254 for ($ip = 0; $ip < $seqIndex; $ip++) {
2255 if (!$inputGlyphs[$ip]) {
2256 $exB .= '[*]';
2257 } else {
2258 $exB .= $this->formatEntityFirst($inputGlyphs[$ip]) . '&#x200d;';
2261 $exB .= '</span>';
2264 if (count($inputGlyphs) > ($seqIndex + 1)) {
2265 $exL .= '<span class="inputother">';
2266 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
2267 if (!$inputGlyphs[$ip]) {
2268 $exL .= '[*]';
2269 } else {
2270 $exL .= $this->formatEntityFirst($inputGlyphs[$ip]) . '&#x200d;';
2273 $exL .= '</span>';
2276 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
2278 $lul2 = [$lup => $tag];
2280 // Only apply if the (first) 'Replace' glyph from the
2281 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2282 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
2283 // to level 2 and only apply if first Replace glyph is in this list
2284 $html .= $this->_getGSUBarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
2286 if (count($subRule['rules'])) {
2287 $volt[] = $subRule;
2291 } // Format 3: Coverage-based Context Glyph Substitution p259
2292 else {
2293 if ($SubstFormat == 3) {
2294 $html .= '<div class="lookuptypesub">Format 3: Coverage-based Context Glyph Substitution </div>';
2295 // IgnoreMarks flag set on main Lookup table
2296 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'];
2297 $CoverageInputGlyphs = implode('|', $inputGlyphs);
2298 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount'];
2300 $exampleI = [];
2301 $html .= '<div class="context">CONTEXT: ';
2302 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
2303 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
2304 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
2306 $html .= '</div>';
2308 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) {
2309 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'];
2310 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'];
2311 // GENERATE exampleI[<seqIndex] .... exampleI[>seqIndex]
2312 $exB = '';
2313 $exL = '';
2314 if ($seqIndex > 0) {
2315 $exB .= '<span class="inputother">';
2316 for ($ip = 0; $ip < $seqIndex; $ip++) {
2317 $exB .= $exampleI[$ip] . '&#x200d;';
2319 $exB .= '</span>';
2322 if (count($inputGlyphs) > ($seqIndex + 1)) {
2323 $exL .= '<span class="inputother">';
2324 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
2325 $exL .= $exampleI[$ip] . '&#x200d;';
2327 $exL .= '</span>';
2330 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
2332 $lul2 = [$lup => $tag];
2334 // Only apply if the (first) 'Replace' glyph from the
2335 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2336 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
2337 // to level 2 and only apply if first Replace glyph is in this list
2338 $html .= $this->_getGSUBarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
2340 if (count($subRule['rules'])) {
2341 $volt[] = $subRule;
2347 //print_r($Lookup[$i]);
2348 //print_r($volt[(count($volt)-1)]); exit;
2349 } // LookupType 6: Chaining Contextual Substitution Subtable
2350 else {
2351 if ($Lookup[$i]['Type'] == 6) {
2352 $html .= '<div class="lookuptype">LookupType 6: Chaining Contextual Substitution Subtable</div>';
2353 // Format 1: Simple Chaining Context Glyph Substitution p255
2354 if ($SubstFormat == 1) {
2355 $html .= '<div class="lookuptypesub">Format 1: Simple Chaining Context Glyph Substitution </div>';
2356 for ($s = 0; $s < $Lookup[$i]['Subtable'][$c]['ChainSubRuleSetCount']; $s++) {
2357 // ChainSubRuleSet
2358 $subRule = [];
2359 $html .= '<div class="rule">Subrule Set: ' . $s . '</div>';
2360 $firstInputGlyph = $Lookup[$i]['Subtable'][$c]['CoverageGlyphs'][$s]; // First input gyyph
2361 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubRuleSet'][$s]['ChainSubRule'] as $rctr => $rule) {
2362 $html .= '<div class="rule">SubRule: ' . $rctr . '</div>';
2363 // ChainSubRule
2364 $inputGlyphs = [];
2365 if ($rule['InputGlyphCount'] > 1) {
2366 $inputGlyphs = $rule['InputGlyphs'];
2368 $inputGlyphs[0] = $firstInputGlyph;
2369 ksort($inputGlyphs);
2370 $nInput = count($inputGlyphs);
2372 if ($rule['BacktrackGlyphCount']) {
2373 $backtrackGlyphs = $rule['BacktrackGlyphs'];
2374 } else {
2375 $backtrackGlyphs = [];
2378 if ($rule['LookaheadGlyphCount']) {
2379 $lookaheadGlyphs = $rule['LookaheadGlyphs'];
2380 } else {
2381 $lookaheadGlyphs = [];
2384 $exampleB = [];
2385 $exampleI = [];
2386 $exampleL = [];
2387 $html .= '<div class="context">CONTEXT: ';
2388 for ($ff = count($backtrackGlyphs) - 1; $ff >= 0; $ff--) {
2389 $html .= '<div>Backtrack #' . $ff . ': <span class="unicode">' . $this->formatUniStr($backtrackGlyphs[$ff]) . '</span></div>';
2390 $exampleB[] = $this->formatEntityFirst($backtrackGlyphs[$ff]);
2392 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
2393 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
2394 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
2396 for ($ff = 0; $ff < count($lookaheadGlyphs); $ff++) {
2397 $html .= '<div>Lookahead #' . $ff . ': <span class="unicode">' . $this->formatUniStr($lookaheadGlyphs[$ff]) . '</span></div>';
2398 $exampleL[] = $this->formatEntityFirst($lookaheadGlyphs[$ff]);
2400 $html .= '</div>';
2402 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2403 $lup = $rule['LookupListIndex'][$b];
2404 $seqIndex = $rule['SequenceIndex'][$b];
2406 // GENERATE exampleB[n] exampleI[<seqIndex] .... exampleI[>seqIndex] exampleL[n]
2407 $exB = '';
2408 $exL = '';
2409 if (count($exampleB)) {
2410 $exB .= '<span class="backtrack">' . implode('&#x200d;', $exampleB) . '</span>';
2413 if ($seqIndex > 0) {
2414 $exB .= '<span class="inputother">';
2415 for ($ip = 0; $ip < $seqIndex; $ip++) {
2416 $exB .= $this->formatEntity($inputGlyphs[$ip]) . '&#x200d;';
2418 $exB .= '</span>';
2421 if (count($inputGlyphs) > ($seqIndex + 1)) {
2422 $exL .= '<span class="inputother">';
2423 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
2424 $exL .= $this->formatEntity($inputGlyphs[$ip]) . '&#x200d;';
2426 $exL .= '</span>';
2429 if (count($exampleL)) {
2430 $exL .= '<span class="lookahead">' . implode('&#x200d;', $exampleL) . '</span>';
2433 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
2435 $lul2 = [$lup => $tag];
2437 // Only apply if the (first) 'Replace' glyph from the
2438 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2439 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
2440 // to level 2 and only apply if first Replace glyph is in this list
2441 $html .= $this->_getGSUBarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
2444 if (count($subRule['rules'])) {
2445 $volt[] = $subRule;
2449 } // Format 2: Class-based Chaining Context Glyph Substitution p257
2450 else {
2451 if ($SubstFormat == 2) {
2452 $html .= '<div class="lookuptypesub">Format 2: Class-based Chaining Context Glyph Substitution </div>';
2453 foreach ($Lookup[$i]['Subtable'][$c]['ChainSubClassSet'] as $inputClass => $cscs) {
2454 $html .= '<div class="rule">Input Class: ' . $inputClass . '</div>';
2455 for ($cscrule = 0; $cscrule < $cscs['ChainSubClassRuleCnt']; $cscrule++) {
2456 $html .= '<div class="rule">Rule: ' . $cscrule . '</div>';
2457 $rule = $cscs['ChainSubClassRule'][$cscrule];
2459 // These contain classes of glyphs as strings
2460 // $Lookup[$i]['Subtable'][$c]['InputClasses'][(class)] e.g. 02E6|02E7|02E8
2461 // $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][(class)]
2462 // $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][(class)]
2463 // These contain arrays of classIndexes
2464 // [Backtrack] [Lookahead] and [Input] (Input is from the second position only)
2466 $inputGlyphs = [];
2468 $inputGlyphs[0] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$inputClass];
2469 if ($rule['InputGlyphCount'] > 1) {
2470 // NB starts at 1
2471 for ($gcl = 1; $gcl < $rule['InputGlyphCount']; $gcl++) {
2472 $classindex = $rule['Input'][$gcl];
2473 $inputGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['InputClasses'][$classindex];
2476 // Class 0 contains all the glyphs NOT in the other classes
2477 $class0excl = implode('|', $Lookup[$i]['Subtable'][$c]['InputClasses']);
2479 $nInput = $rule['InputGlyphCount'];
2481 if ($rule['BacktrackGlyphCount']) {
2482 for ($gcl = 0; $gcl < $rule['BacktrackGlyphCount']; $gcl++) {
2483 $classindex = $rule['Backtrack'][$gcl];
2484 $backtrackGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['BacktrackClasses'][$classindex];
2486 } else {
2487 $backtrackGlyphs = [];
2490 if ($rule['LookaheadGlyphCount']) {
2491 for ($gcl = 0; $gcl < $rule['LookaheadGlyphCount']; $gcl++) {
2492 $classindex = $rule['Lookahead'][$gcl];
2493 $lookaheadGlyphs[$gcl] = $Lookup[$i]['Subtable'][$c]['LookaheadClasses'][$classindex];
2495 } else {
2496 $lookaheadGlyphs = [];
2499 $exampleB = [];
2500 $exampleI = [];
2501 $exampleL = [];
2502 $html .= '<div class="context">CONTEXT: ';
2503 for ($ff = count($backtrackGlyphs) - 1; $ff >= 0; $ff--) {
2504 if (!$backtrackGlyphs[$ff]) {
2505 $html .= '<div>Backtrack #' . $ff . ': <span class="unchanged">&nbsp;[NOT ' . $this->formatEntityStr($class0excl) . ']&nbsp;</span></div>';
2506 $exampleB[] = '[NOT ' . $this->formatEntityFirst($class0excl) . ']';
2507 } else {
2508 $html .= '<div>Backtrack #' . $ff . ': <span class="unicode">' . $this->formatUniStr($backtrackGlyphs[$ff]) . '</span></div>';
2509 $exampleB[] = $this->formatEntityFirst($backtrackGlyphs[$ff]);
2512 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
2513 if (!$inputGlyphs[$ff]) {
2514 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;[NOT ' . $this->formatEntityStr($class0excl) . ']&nbsp;</span></div>';
2515 $exampleI[] = '[NOT ' . $this->formatEntityFirst($class0excl) . ']';
2516 } else {
2517 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
2518 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
2521 for ($ff = 0; $ff < count($lookaheadGlyphs); $ff++) {
2522 if (!$lookaheadGlyphs[$ff]) {
2523 $html .= '<div>Lookahead #' . $ff . ': <span class="unchanged">&nbsp;[NOT ' . $this->formatEntityStr($class0excl) . ']&nbsp;</span></div>';
2524 $exampleL[] = '[NOT ' . $this->formatEntityFirst($class0excl) . ']';
2525 } else {
2526 $html .= '<div>Lookahead #' . $ff . ': <span class="unicode">' . $this->formatUniStr($lookaheadGlyphs[$ff]) . '</span></div>';
2527 $exampleL[] = $this->formatEntityFirst($lookaheadGlyphs[$ff]);
2530 $html .= '</div>';
2532 for ($b = 0; $b < $rule['SubstCount']; $b++) {
2533 $lup = $rule['LookupListIndex'][$b];
2534 $seqIndex = $rule['SequenceIndex'][$b];
2536 // GENERATE exampleB[n] exampleI[<seqIndex] .... exampleI[>seqIndex] exampleL[n]
2537 $exB = '';
2538 $exL = '';
2539 if (count($exampleB)) {
2540 $exB .= '<span class="backtrack">' . implode('&#x200d;', $exampleB) . '</span>';
2543 if ($seqIndex > 0) {
2544 $exB .= '<span class="inputother">';
2545 for ($ip = 0; $ip < $seqIndex; $ip++) {
2546 if (!$inputGlyphs[$ip]) {
2547 $exB .= '[*]';
2548 } else {
2549 $exB .= $this->formatEntityFirst($inputGlyphs[$ip]) . '&#x200d;';
2552 $exB .= '</span>';
2555 if (count($inputGlyphs) > ($seqIndex + 1)) {
2556 $exL .= '<span class="inputother">';
2557 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
2558 if (!$inputGlyphs[$ip]) {
2559 $exL .= '[*]';
2560 } else {
2561 $exL .= $this->formatEntityFirst($inputGlyphs[$ip]) . '&#x200d;';
2564 $exL .= '</span>';
2567 if (count($exampleL)) {
2568 $exL .= '<span class="lookahead">' . implode('&#x200d;', $exampleL) . '</span>';
2571 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
2573 $lul2 = [$lup => $tag];
2575 // Only apply if the (first) 'Replace' glyph from the
2576 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2577 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
2578 // to level 2 and only apply if first Replace glyph is in this list
2579 $html .= $this->_getGSUBarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
2584 //print_r($Lookup[$i]['Subtable'][$c]); exit;
2585 } // Format 3: Coverage-based Chaining Context Glyph Substitution p259
2586 else {
2587 if ($SubstFormat == 3) {
2588 $html .= '<div class="lookuptypesub">Format 3: Coverage-based Chaining Context Glyph Substitution </div>';
2589 // IgnoreMarks flag set on main Lookup table
2590 $inputGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageInputGlyphs'];
2591 $CoverageInputGlyphs = implode('|', $inputGlyphs);
2592 $nInput = $Lookup[$i]['Subtable'][$c]['InputGlyphCount'];
2594 if ($Lookup[$i]['Subtable'][$c]['BacktrackGlyphCount']) {
2595 $backtrackGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageBacktrackGlyphs'];
2596 } else {
2597 $backtrackGlyphs = [];
2600 if ($Lookup[$i]['Subtable'][$c]['LookaheadGlyphCount']) {
2601 $lookaheadGlyphs = $Lookup[$i]['Subtable'][$c]['CoverageLookaheadGlyphs'];
2602 } else {
2603 $lookaheadGlyphs = [];
2606 $exampleB = [];
2607 $exampleI = [];
2608 $exampleL = [];
2609 $html .= '<div class="context">CONTEXT: ';
2610 for ($ff = count($backtrackGlyphs) - 1; $ff >= 0; $ff--) {
2611 $html .= '<div>Backtrack #' . $ff . ': <span class="unicode">' . $this->formatUniStr($backtrackGlyphs[$ff]) . '</span></div>';
2612 $exampleB[] = $this->formatEntityFirst($backtrackGlyphs[$ff]);
2614 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
2615 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
2616 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
2618 for ($ff = 0; $ff < count($lookaheadGlyphs); $ff++) {
2619 $html .= '<div>Lookahead #' . $ff . ': <span class="unicode">' . $this->formatUniStr($lookaheadGlyphs[$ff]) . '</span></div>';
2620 $exampleL[] = $this->formatEntityFirst($lookaheadGlyphs[$ff]);
2622 $html .= '</div>';
2624 for ($b = 0; $b < $Lookup[$i]['Subtable'][$c]['SubstCount']; $b++) {
2625 $lup = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['LookupListIndex'];
2626 $seqIndex = $Lookup[$i]['Subtable'][$c]['SubstLookupRecord'][$b]['SequenceIndex'];
2628 // GENERATE exampleB[n] exampleI[<seqIndex] .... exampleI[>seqIndex] exampleL[n]
2629 $exB = '';
2630 $exL = '';
2631 if (count($exampleB)) {
2632 $exB .= '<span class="backtrack">' . implode('&#x200d;', $exampleB) . '</span>';
2635 if ($seqIndex > 0) {
2636 $exB .= '<span class="inputother">';
2637 for ($ip = 0; $ip < $seqIndex; $ip++) {
2638 $exB .= $exampleI[$ip] . '&#x200d;';
2640 $exB .= '</span>';
2643 if (count($inputGlyphs) > ($seqIndex + 1)) {
2644 $exL .= '<span class="inputother">';
2645 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
2646 $exL .= $exampleI[$ip] . '&#x200d;';
2648 $exL .= '</span>';
2651 if (count($exampleL)) {
2652 $exL .= '<span class="lookahead">' . implode('&#x200d;', $exampleL) . '</span>';
2655 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
2657 $lul2 = [$lup => $tag];
2659 // Only apply if the (first) 'Replace' glyph from the
2660 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
2661 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
2662 // to level 2 and only apply if first Replace glyph is in this list
2663 $html .= $this->_getGSUBarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
2675 $html .= '</div>';
2677 if ($level == 1) {
2678 $this->mpdf->WriteHTML($html);
2679 } else {
2680 return $html;
2682 //print_r($Lookup); exit;
2685 //=====================================================================================
2686 //=====================================================================================
2687 // mPDF 5.7.1
2688 function _checkGSUBignore($flag, $glyph, $MarkFilteringSet)
2690 $ignore = false;
2691 // Flag & 0x0008 = Ignore Marks
2692 if ((($flag & 0x0008) == 0x0008) && strpos($this->GlyphClassMarks, $glyph)) {
2693 $ignore = true;
2695 if ((($flag & 0x0004) == 0x0004) && strpos($this->GlyphClassLigatures, $glyph)) {
2696 $ignore = true;
2698 if ((($flag & 0x0002) == 0x0002) && strpos($this->GlyphClassBases, $glyph)) {
2699 $ignore = true;
2701 // Flag & 0xFF?? = MarkAttachmentType
2702 if (($flag & 0xFF00) && strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)) {
2703 $ignore = true;
2705 // Flag & 0x0010 = UseMarkFilteringSet
2706 if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) {
2707 $ignore = true;
2710 return $ignore;
2713 function _getGSUBignoreString($flag, $MarkFilteringSet)
2715 // If ignoreFlag set, combine all ignore glyphs into -> "((?:(?: FBA1| FBA2| FBA3))*)"
2716 // else "()"
2717 // for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup
2718 $str = "";
2719 $ignoreflag = 0;
2721 // Flag & 0xFF?? = MarkAttachmentType
2722 if ($flag & 0xFF00) {
2723 $MarkAttachmentType = $flag >> 8;
2724 $ignoreflag = $flag;
2725 //$str = $this->MarkAttachmentType[$MarkAttachmentType];
2726 $str = "MarkAttachmentType[" . $MarkAttachmentType . "] ";
2729 // Flag & 0x0010 = UseMarkFilteringSet
2730 if ($flag & 0x0010) {
2731 throw new \Mpdf\MpdfException("This font " . $this->fontkey . " contains MarkGlyphSets");
2732 $str = "Mark Glyph Set: ";
2733 $str .= $this->MarkGlyphSets[$MarkFilteringSet];
2736 // If Ignore Marks set, supercedes any above
2737 // Flag & 0x0008 = Ignore Marks
2738 if (($flag & 0x0008) == 0x0008) {
2739 $ignoreflag = 8;
2740 //$str = $this->GlyphClassMarks;
2741 $str = "Mark Glyphs ";
2744 // Flag & 0x0004 = Ignore Ligatures
2745 if (($flag & 0x0004) == 0x0004) {
2746 $ignoreflag += 4;
2747 if ($str) {
2748 $str .= "|";
2750 //$str .= $this->GlyphClassLigatures;
2751 $str .= "Ligature Glyphs ";
2753 // Flag & 0x0002 = Ignore BaseGlyphs
2754 if (($flag & 0x0002) == 0x0002) {
2755 $ignoreflag += 2;
2756 if ($str) {
2757 $str .= "|";
2759 //$str .= $this->GlyphClassBases;
2760 $str .= "Base Glyphs ";
2762 if ($str) {
2763 return $str;
2764 } else {
2765 return "";
2769 // GSUB Patterns
2772 BACKTRACK INPUT LOOKAHEAD
2773 ================================== ================== ==================================
2774 (FEEB|FEEC)(ign) ¦(FD12|FD13)(ign) ¦(0612)¦(ign) (0613)¦(ign) (FD12|FD13)¦(ign) (FEEB|FEEC)
2775 ---------------- ---------------- ----- ------------ --------------- ---------------
2776 Backtrack 1 Backtrack 2 Input 1 Input 2 Lookahead 1 Lookahead 2
2777 -------- --- --------- --- ---- --- ---- --- --------- --- -------
2778 \${1} \${2} \${3} \${4} \${5+} \${6+} \${7+} \${8+}
2780 nBacktrack = 2 nInput = 2 nLookahead = 2
2782 nBsubs = 2xnBack nIsubs = (nBsubs+) nLsubs = (nBsubs+nIsubs+) 2xnLookahead
2783 "\${1}\${2} " (nInput*2)-1 "\${5+} \${6+}"
2784 "REPL"
2786 ¦\${1}\${2} ¦\${3}\${4} ¦REPL¦\${5+} \${6+}¦\${7+} \${8+}¦
2789 INPUT nInput = 5
2790 ============================================================
2791 ¦(0612)¦(ign) (0613)¦(ign) (0614)¦(ign) (0615)¦(ign) (0615)¦
2792 \${1} \${2} \${3} \${4} \${5} \${6} \${7} \${8} \${9} (All backreference numbers are + nBsubs)
2793 ----- ------------ ------------ ------------ ------------
2794 Input 1 Input 2 Input 3 Input 4 Input 5
2796 A====== SequenceIndex=1 ; Lookup match nGlyphs=1
2797 B=================== SequenceIndex=1 ; Lookup match nGlyphs=2
2798 C=============================== SequenceIndex=1 ; Lookup match nGlyphs=3
2799 D======================= SequenceIndex=2 ; Lookup match nGlyphs=2
2800 E===================================== SequenceIndex=2 ; Lookup match nGlyphs=3
2801 F====================== SequenceIndex=4 ; Lookup match nGlyphs=2
2803 All backreference numbers are + nBsubs
2804 A - "REPL\${2} \${3}\${4} \${5}\${6} \${7}\${8} \${9}"
2805 B - "REPL\${2}\${4} \${5}\${6} \${7}\${8} \${9}"
2806 C - "REPL\${2}\${4}\${6} \${7}\${8} \${9}"
2807 D - "\${1} REPL\${2}\${4}\${6} \${7}\${8} \${9}"
2808 E - "\${1} REPL\${2}\${4}\${6}\${8} \${9}"
2809 F - "\${1}\${2} \${3}\${4} \${5} REPL\${6}\${8}"
2812 function _makeGSUBcontextInputMatch($inputGlyphs, $ignore, $lookupGlyphs, $seqIndex)
2814 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
2815 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2816 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context
2817 // $lookupGlyphs = array of glyphs (single Glyphs) making up Lookup Input sequence
2818 $mLen = count($lookupGlyphs); // nGlyphs in the secondary Lookup match
2819 $nInput = count($inputGlyphs); // nGlyphs in the Primary Input sequence
2820 $str = "";
2821 for ($i = 0; $i < $nInput; $i++) {
2822 if ($i > 0) {
2823 $str .= $ignore . " ";
2825 if ($i >= $seqIndex && $i < ($seqIndex + $mLen)) {
2826 $str .= "" . $lookupGlyphs[($i - $seqIndex)] . "";
2827 } else {
2828 $str .= "" . $inputGlyphs[($i)] . "";
2832 return $str;
2835 function _makeGSUBinputMatch($inputGlyphs, $ignore)
2837 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
2838 // Returns e.g. ¦(0612)¦(ignore) (0613)¦(ignore) (0614)¦
2839 // $inputGlyphs = array of glyphs(glyphstrings) making up Input sequence in Context
2840 // $lookupGlyphs = array of glyphs making up Lookup Input sequence - if applicable
2841 $str = "";
2842 for ($i = 1; $i <= count($inputGlyphs); $i++) {
2843 if ($i > 1) {
2844 $str .= $ignore . " ";
2846 $str .= "" . $inputGlyphs[($i - 1)] . "";
2849 return $str;
2852 function _makeGSUBbacktrackMatch($backtrackGlyphs, $ignore)
2854 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
2855 // Returns e.g. ¦(FEEB|FEEC)(ignore) ¦(FD12|FD13)(ignore) ¦
2856 // $backtrackGlyphs = array of glyphstrings making up Backtrack sequence
2857 // 3 2 1 0
2858 // each item being e.g. E0AD|E0AF|F1FD
2859 $str = "";
2860 for ($i = (count($backtrackGlyphs) - 1); $i >= 0; $i--) {
2861 $str .= "" . $backtrackGlyphs[$i] . " " . $ignore . " ";
2864 return $str;
2867 function _makeGSUBlookaheadMatch($lookaheadGlyphs, $ignore)
2869 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
2870 // Returns e.g. ¦(ignore) (FD12|FD13)¦(ignore) (FEEB|FEEC)¦
2871 // $lookaheadGlyphs = array of glyphstrings making up Lookahead sequence
2872 // 0 1 2 3
2873 // each item being e.g. E0AD|E0AF|F1FD
2874 $str = "";
2875 for ($i = 0; $i < count($lookaheadGlyphs); $i++) {
2876 $str .= $ignore . " " . $lookaheadGlyphs[$i] . "";
2879 return $str;
2882 function _makeGSUBinputReplacement($nInput, $REPL, $ignore, $nBsubs, $mLen, $seqIndex)
2884 // Returns e.g. "REPL\${6}\${8}" or "\${1}\${2} \${3} REPL\${4}\${6}\${8} \${9}"
2885 // $nInput nGlyphs in the Primary Input sequence
2886 // $REPL replacement glyphs from secondary lookup
2887 // $ignore = "((?:(?: FBA1| FBA2| FBA3))*)" or "()"
2888 // $nBsubs Number of Backtrack substitutions (= 2x Number of Backtrack glyphs)
2889 // $mLen nGlyphs in the secondary Lookup match - if no secondary lookup, should=$nInput
2890 // $seqIndex Sequence Index to apply the secondary match
2891 if ($ignore == "()") {
2892 $ign = false;
2893 } else {
2894 $ign = true;
2896 $str = "";
2897 if ($nInput == 1) {
2898 $str = $REPL;
2899 } else {
2900 if ($nInput > 1) {
2901 if ($mLen == $nInput) { // whole string replaced
2902 $str = $REPL;
2903 if ($ign) {
2904 // for every nInput over 1, add another replacement backreference, to move IGNORES after replacement
2905 for ($x = 2; $x <= $nInput; $x++) {
2906 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
2909 } else { // if only part of string replaced:
2910 for ($x = 1; $x < ($seqIndex + 1); $x++) {
2911 if ($x == 1) {
2912 $str .= '\\' . ($nBsubs + 1);
2913 } else {
2914 if ($ign) {
2915 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
2917 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1)));
2920 if ($seqIndex > 0) {
2921 $str .= " ";
2923 $str .= $REPL;
2924 if ($ign) {
2925 for ($x = (max(($seqIndex + 1), 2)); $x < ($seqIndex + 1 + $mLen); $x++) { // move IGNORES after replacement
2926 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
2929 for ($x = ($seqIndex + 1 + $mLen); $x <= $nInput; $x++) {
2930 if ($ign) {
2931 $str .= '\\' . ($nBsubs + (2 * ($x - 1)));
2933 $str .= ' \\' . ($nBsubs + 1 + (2 * ($x - 1)));
2939 return $str;
2942 //////////////////////////////////////////////////////////////////////////////////
2943 function _getCoverage($convert2hex = true)
2945 $g = [];
2946 $CoverageFormat = $this->read_ushort();
2947 if ($CoverageFormat == 1) {
2948 $CoverageGlyphCount = $this->read_ushort();
2949 for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) {
2950 $glyphID = $this->read_ushort();
2951 if ($convert2hex) {
2952 $g[] = unicode_hex($this->glyphToChar[$glyphID][0]);
2953 } else {
2954 $g[] = $glyphID;
2958 if ($CoverageFormat == 2) {
2959 $RangeCount = $this->read_ushort();
2960 for ($r = 0; $r < $RangeCount; $r++) {
2961 $start = $this->read_ushort();
2962 $end = $this->read_ushort();
2963 $StartCoverageIndex = $this->read_ushort(); // n/a
2964 for ($gid = $start; $gid <= $end; $gid++) {
2965 $glyphID = $gid;
2966 if ($convert2hex) {
2967 $g[] = unicode_hex($this->glyphToChar[$glyphID][0]);
2968 } else {
2969 $g[] = $glyphID;
2975 return $g;
2978 //////////////////////////////////////////////////////////////////////////////////
2979 function _getClasses($offset)
2981 $this->seek($offset);
2982 $ClassFormat = $this->read_ushort();
2983 $GlyphByClass = [];
2984 if ($ClassFormat == 1) {
2985 $StartGlyph = $this->read_ushort();
2986 $GlyphCount = $this->read_ushort();
2987 for ($i = 0; $i < $GlyphCount; $i++) {
2988 $startGlyphID = $StartGlyph + $i;
2989 $endGlyphID = $StartGlyph + $i;
2990 $class = $this->read_ushort();
2991 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
2992 if ($this->glyphToChar[$g][0]) {
2993 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]);
2997 } else {
2998 if ($ClassFormat == 2) {
2999 $tableCount = $this->read_ushort();
3000 for ($i = 0; $i < $tableCount; $i++) {
3001 $startGlyphID = $this->read_ushort();
3002 $endGlyphID = $this->read_ushort();
3003 $class = $this->read_ushort();
3004 for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
3005 if ($this->glyphToChar[$g][0]) {
3006 $GlyphByClass[$class][] = unicode_hex($this->glyphToChar[$g][0]);
3012 $gbc = [];
3013 foreach ($GlyphByClass as $class => $garr) {
3014 $gbc[$class] = implode('|', $garr);
3017 return $gbc;
3020 //////////////////////////////////////////////////////////////////////////////////
3021 //////////////////////////////////////////////////////////////////////////////////
3022 //////////////////////////////////////////////////////////////////////////////////
3023 //////////////////////////////////////////////////////////////////////////////////
3024 //////////////////////////////////////////////////////////////////////////////////
3025 function _getGPOStables()
3027 ///////////////////////////////////
3028 // GPOS - Glyph Positioning
3029 ///////////////////////////////////
3030 if (isset($this->tables["GPOS"])) {
3031 $this->mpdf->WriteHTML('<h1>GPOS Tables</h1>');
3032 $ffeats = [];
3033 $gpos_offset = $this->seek_table("GPOS");
3034 $this->skip(4);
3035 $ScriptList_offset = $gpos_offset + $this->read_ushort();
3036 $FeatureList_offset = $gpos_offset + $this->read_ushort();
3037 $LookupList_offset = $gpos_offset + $this->read_ushort();
3039 // ScriptList
3040 $this->seek($ScriptList_offset);
3041 $ScriptCount = $this->read_ushort();
3042 for ($i = 0; $i < $ScriptCount; $i++) {
3043 $ScriptTag = $this->read_tag(); // = "beng", "deva" etc.
3044 $ScriptTableOffset = $this->read_ushort();
3045 $ffeats[$ScriptTag] = $ScriptList_offset + $ScriptTableOffset;
3048 // Script Table
3049 foreach ($ffeats as $t => $o) {
3050 $ls = [];
3051 $this->seek($o);
3052 $DefLangSys_offset = $this->read_ushort();
3053 if ($DefLangSys_offset > 0) {
3054 $ls['DFLT'] = $DefLangSys_offset + $o;
3056 $LangSysCount = $this->read_ushort();
3057 for ($i = 0; $i < $LangSysCount; $i++) {
3058 $LangTag = $this->read_tag(); // =
3059 $LangTableOffset = $this->read_ushort();
3060 $ls[$LangTag] = $o + $LangTableOffset;
3062 $ffeats[$t] = $ls;
3065 // Get FeatureIndexList
3066 // LangSys Table - from first listed langsys
3067 foreach ($ffeats as $st => $scripts) {
3068 foreach ($scripts as $t => $o) {
3069 $FeatureIndex = [];
3070 $langsystable_offset = $o;
3071 $this->seek($langsystable_offset);
3072 $LookUpOrder = $this->read_ushort(); //==NULL
3073 $ReqFeatureIndex = $this->read_ushort();
3074 if ($ReqFeatureIndex != 0xFFFF) {
3075 $FeatureIndex[] = $ReqFeatureIndex;
3077 $FeatureCount = $this->read_ushort();
3078 for ($i = 0; $i < $FeatureCount; $i++) {
3079 $FeatureIndex[] = $this->read_ushort(); // = index of feature
3081 $ffeats[$st][$t] = $FeatureIndex;
3084 //print_r($ffeats); exit;
3085 // Feauture List => LookupListIndex es
3086 $this->seek($FeatureList_offset);
3087 $FeatureCount = $this->read_ushort();
3088 $Feature = [];
3089 for ($i = 0; $i < $FeatureCount; $i++) {
3090 $Feature[$i] = ['tag' => $this->read_tag()];
3091 $Feature[$i]['offset'] = $FeatureList_offset + $this->read_ushort();
3093 for ($i = 0; $i < $FeatureCount; $i++) {
3094 $this->seek($Feature[$i]['offset']);
3095 $this->read_ushort(); // null
3096 $Feature[$i]['LookupCount'] = $Lookupcount = $this->read_ushort();
3097 $Feature[$i]['LookupListIndex'] = [];
3098 for ($c = 0; $c < $Lookupcount; $c++) {
3099 $Feature[$i]['LookupListIndex'][] = $this->read_ushort();
3103 foreach ($ffeats as $st => $scripts) {
3104 foreach ($scripts as $t => $o) {
3105 $FeatureIndex = $ffeats[$st][$t];
3106 foreach ($FeatureIndex as $k => $fi) {
3107 $ffeats[$st][$t][$k] = $Feature[$fi];
3111 //print_r($ffeats); exit;
3112 //=====================================================================================
3113 $gpos = [];
3114 $GPOSScriptLang = [];
3115 foreach ($ffeats as $st => $scripts) {
3116 foreach ($scripts as $t => $langsys) {
3117 $lg = [];
3118 foreach ($langsys as $ft) {
3119 $lg[$ft['LookupListIndex'][0]] = $ft;
3121 // list of Lookups in order they need to be run i.e. order listed in Lookup table
3122 ksort($lg);
3123 foreach ($lg as $ft) {
3124 $gpos[$st][$t][$ft['tag']] = $ft['LookupListIndex'];
3126 if (!isset($GPOSScriptLang[$st])) {
3127 $GPOSScriptLang[$st] = '';
3129 $GPOSScriptLang[$st] .= $t . ' ';
3132 if ($this->mode == 'summary') {
3133 $this->mpdf->WriteHTML('<h3>GPOS Scripts &amp; Languages</h3>');
3134 $html = '';
3135 if (count($gpos)) {
3136 foreach ($gpos as $st => $g) {
3137 $html .= '<h5>' . $st . '</h5>';
3138 foreach ($g as $l => $t) {
3139 $html .= '<div><a href="font_dump_OTL.php?script=' . $st . '&lang=' . $l . '">' . $l . '</a></b>: ';
3140 foreach ($t as $tag => $o) {
3141 $html .= $tag . ' ';
3143 $html .= '</div>';
3146 } else {
3147 $html .= '<div>No entries in GPOS table.</div>';
3149 $this->mpdf->WriteHTML($html);
3150 $this->mpdf->WriteHTML('</div>');
3152 return 0;
3155 //=====================================================================================
3156 // Get metadata and offsets for whole Lookup List table
3157 $this->seek($LookupList_offset);
3158 $LookupCount = $this->read_ushort();
3159 $Lookup = [];
3160 $Offsets = [];
3161 $SubtableCount = [];
3162 for ($i = 0; $i < $LookupCount; $i++) {
3163 $Offsets[$i] = $LookupList_offset + $this->read_ushort();
3165 for ($i = 0; $i < $LookupCount; $i++) {
3166 $this->seek($Offsets[$i]);
3167 $Lookup[$i]['Type'] = $this->read_ushort();
3168 $Lookup[$i]['Flag'] = $flag = $this->read_ushort();
3169 $Lookup[$i]['SubtableCount'] = $SubtableCount[$i] = $this->read_ushort();
3170 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
3171 $Lookup[$i]['Subtables'][$c] = $Offsets[$i] + $this->read_ushort();
3173 // MarkFilteringSet = Index (base 0) into GDEF mark glyph sets structure
3174 if (($flag & 0x0010) == 0x0010) {
3175 $Lookup[$i]['MarkFilteringSet'] = $this->read_ushort();
3177 // else { $Lookup[$i]['MarkFilteringSet'] = ''; }
3178 // Lookup Type 9: Extension
3179 if ($Lookup[$i]['Type'] == 9) {
3180 // Overwrites new offset (32-bit) for each subtable, and a new lookup Type
3181 for ($c = 0; $c < $SubtableCount[$i]; $c++) {
3182 $this->seek($Lookup[$i]['Subtables'][$c]);
3183 $ExtensionPosFormat = $this->read_ushort();
3184 $type = $this->read_ushort();
3185 $Lookup[$i]['Subtables'][$c] = $Lookup[$i]['Subtables'][$c] + $this->read_ulong();
3187 $Lookup[$i]['Type'] = $type;
3191 //=====================================================================================
3193 $st = $this->mpdf->OTLscript;
3194 $t = $this->mpdf->OTLlang;
3195 $langsys = $gpos[$st][$t];
3197 $lul = []; // array of LookupListIndexes
3198 $tags = []; // corresponding array of feature tags e.g. 'ccmp'
3199 if (count($langsys)) {
3200 foreach ($langsys as $tag => $ft) {
3201 foreach ($ft as $ll) {
3202 $lul[$ll] = $tag;
3206 ksort($lul); // Order the Lookups in the order they are in the GUSB table, regardless of Feature order
3207 $this->_getGPOSarray($Lookup, $lul, $st);
3209 //print_r($lul); exit;
3211 return [$GPOSScriptLang, $gpos, $Lookup];
3212 } // end if GPOS
3215 //////////////////////////////////////////////////////////////////////////////////
3216 //=====================================================================================
3217 //=====================================================================================
3218 //=====================================================================================
3219 /////////////////////////////////////////////////////////////////////////////////////////
3220 // GPOS functions
3221 function _getGPOSarray(&$Lookup, $lul, $scripttag, $level = 1, $lcoverage = '', $exB = '', $exL = '')
3223 // Process (3) LookupList for specific Script-LangSys
3224 $html = '';
3225 if ($level == 1) {
3226 $html .= '<bookmark level="0" content="GPOS features">';
3228 foreach ($lul as $luli => $tag) {
3229 $html .= '<div class="level' . $level . '">';
3230 $html .= '<h5 class="level' . $level . '">';
3231 if ($level == 1) {
3232 $html .= '<bookmark level="1" content="' . $tag . ' [#' . $luli . ']">';
3234 $html .= 'Lookup #' . $luli . ' [tag: <span style="color:#000066;">' . $tag . '</span>]</h5>';
3235 $ignore = $this->_getGSUBignoreString($Lookup[$luli]['Flag'], $Lookup[$luli]['MarkFilteringSet']);
3236 if ($ignore) {
3237 $html .= '<div class="ignore">Ignoring: ' . $ignore . '</div> ';
3240 $Type = $Lookup[$luli]['Type'];
3241 $Flag = $Lookup[$luli]['Flag'];
3242 if (($Flag & 0x0001) == 1) {
3243 $dir = 'RTL';
3244 } else {
3245 $dir = 'LTR';
3248 for ($c = 0; $c < $Lookup[$luli]['SubtableCount']; $c++) {
3249 $html .= '<div class="subtable">Subtable #' . $c;
3250 if ($level == 1) {
3251 $html .= '<bookmark level="2" content="Subtable #' . $c . '">';
3253 $html .= '</div>';
3255 // Lets start
3256 $subtable_offset = $Lookup[$luli]['Subtables'][$c];
3257 $this->seek($subtable_offset);
3258 $PosFormat = $this->read_ushort();
3260 ////////////////////////////////////////////////////////////////////////////////
3261 // LookupType 1: Single adjustment Adjust position of a single glyph (e.g. SmallCaps/Sups/Subs)
3262 ////////////////////////////////////////////////////////////////////////////////
3263 if ($Lookup[$luli]['Type'] == 1) {
3264 $html .= '<div class="lookuptype">LookupType 1: Single adjustment [Format ' . $PosFormat . ']</div>';
3265 //===========
3266 // Format 1:
3267 //===========
3268 if ($PosFormat == 1) {
3269 $Coverage = $subtable_offset + $this->read_ushort();
3270 $ValueFormat = $this->read_ushort();
3271 $Value = $this->_getValueRecord($ValueFormat);
3273 $this->seek($Coverage);
3274 $glyphs = $this->_getCoverage(); // Array of Hex Glyphs
3275 for ($g = 0; $g < count($glyphs); $g++) {
3276 if ($level == 2 && strpos($lcoverage, $glyphs[$g]) === false) {
3277 continue;
3280 $html .= '<div class="substitution">';
3281 $html .= '<span class="unicode">' . $this->formatUni($glyphs[$g]) . '&nbsp;</span> ';
3282 if ($level == 2 && $exB) {
3283 $html .= $exB;
3285 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($glyphs[$g]) . '</span>';
3286 if ($level == 2 && $exL) {
3287 $html .= $exL;
3289 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
3290 if ($level == 2 && $exB) {
3291 $html .= $exB;
3293 $html .= '<span class="changed" style="font-feature-settings:\'' . $tag . '\' 1;">&nbsp;' . $this->formatEntity($glyphs[$g]) . '</span>';
3294 if ($level == 2 && $exL) {
3295 $html .= $exL;
3297 $html .= ' <span class="unicode">';
3298 if ($Value['XPlacement']) {
3299 $html .= ' Xpl: ' . $Value['XPlacement'] . ';';
3301 if ($Value['YPlacement']) {
3302 $html .= ' YPl: ' . $Value['YPlacement'] . ';';
3304 if ($Value['XAdvance']) {
3305 $html .= ' Xadv: ' . $Value['XAdvance'];
3307 $html .= '</span>';
3308 $html .= '</div>';
3310 } //===========
3311 // Format 2:
3312 //===========
3313 else {
3314 if ($PosFormat == 2) {
3315 $Coverage = $subtable_offset + $this->read_ushort();
3316 $ValueFormat = $this->read_ushort();
3317 $ValueCount = $this->read_ushort();
3318 $Values = [];
3319 for ($v = 0; $v < $ValueCount; $v++) {
3320 $Values[] = $this->_getValueRecord($ValueFormat);
3323 $this->seek($Coverage);
3324 $glyphs = $this->_getCoverage(); // Array of Hex Glyphs
3326 for ($g = 0; $g < count($glyphs); $g++) {
3327 if ($level == 2 && strpos($lcoverage, $glyphs[$g]) === false) {
3328 continue;
3330 $Value = $Values[$g];
3332 $html .= '<div class="substitution">';
3333 $html .= '<span class="unicode">' . $this->formatUni($glyphs[$g]) . '&nbsp;</span> ';
3334 if ($level == 2 && $exB) {
3335 $html .= $exB;
3337 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($glyphs[$g]) . '</span>';
3338 if ($level == 2 && $exL) {
3339 $html .= $exL;
3341 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
3342 if ($level == 2 && $exB) {
3343 $html .= $exB;
3345 $html .= '<span class="changed" style="font-feature-settings:\'' . $tag . '\' 1;">&nbsp;' . $this->formatEntity($glyphs[$g]) . '</span>';
3346 if ($level == 2 && $exL) {
3347 $html .= $exL;
3349 $html .= ' <span class="unicode">';
3350 if ($Value['XPlacement']) {
3351 $html .= ' Xpl: ' . $Value['XPlacement'] . ';';
3353 if ($Value['YPlacement']) {
3354 $html .= ' YPl: ' . $Value['YPlacement'] . ';';
3356 if ($Value['XAdvance']) {
3357 $html .= ' Xadv: ' . $Value['XAdvance'];
3359 $html .= '</span>';
3360 $html .= '</div>';
3364 } ////////////////////////////////////////////////////////////////////////////////
3365 // LookupType 2: Pair adjustment Adjust position of a pair of glyphs (Kerning)
3366 ////////////////////////////////////////////////////////////////////////////////
3367 else {
3368 if ($Lookup[$luli]['Type'] == 2) {
3369 $html .= '<div class="lookuptype">LookupType 2: Pair adjustment e.g. Kerning [Format ' . $PosFormat . ']</div>';
3370 $Coverage = $subtable_offset + $this->read_ushort();
3371 $ValueFormat1 = $this->read_ushort();
3372 $ValueFormat2 = $this->read_ushort();
3373 //===========
3374 // Format 1:
3375 //===========
3376 if ($PosFormat == 1) {
3377 $PairSetCount = $this->read_ushort();
3378 $PairSetOffset = [];
3379 for ($p = 0; $p < $PairSetCount; $p++) {
3380 $PairSetOffset[] = $subtable_offset + $this->read_ushort();
3382 $this->seek($Coverage);
3383 $glyphs = $this->_getCoverage(); // Array of Hex Glyphs
3384 for ($p = 0; $p < $PairSetCount; $p++) {
3385 if ($level == 2 && strpos($lcoverage, $glyphs[$p]) === false) {
3386 continue;
3388 $this->seek($PairSetOffset[$p]);
3389 // First Glyph = $glyphs[$p]
3390 // Takes too long e.g. Calibri font - just list kerning pairs with this:
3391 $html .= '<div class="glyphs">';
3392 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($glyphs[$p]) . ' </span>';
3394 //PairSet table
3395 $PairValueCount = $this->read_ushort();
3396 for ($pv = 0; $pv < $PairValueCount; $pv++) {
3397 //PairValueRecord
3398 $gid = $this->read_ushort();
3399 $SecondGlyph = unicode_hex($this->glyphToChar[$gid][0]);
3400 $Value1 = $this->_getValueRecord($ValueFormat1);
3401 $Value2 = $this->_getValueRecord($ValueFormat2);
3403 // If RTL pairs, GPOS declares a XPlacement e.g. -180 for an XAdvance of -180 to take
3404 // account of direction. mPDF does not need the XPlacement adjustment
3405 if ($dir == 'RTL' && $Value1['XPlacement']) {
3406 $Value1['XPlacement'] -= $Value1['XAdvance'];
3409 if ($ValueFormat2) {
3410 // If RTL pairs, GPOS declares a XPlacement e.g. -180 for an XAdvance of -180 to take
3411 // account of direction. mPDF does not need the XPlacement adjustment
3412 if ($dir == 'RTL' && $Value2['XPlacement'] && $Value2['XAdvance']) {
3413 $Value2['XPlacement'] -= $Value2['XAdvance'];
3417 $html .= ' ' . $this->formatEntity($SecondGlyph) . ' ';
3420 $html .= '<div class="substitution">';
3421 $html .= '<span class="unicode">'.$this->formatUni($glyphs[$p]).'&nbsp;</span> ';
3422 if ($level==2 && $exB) { $html .= $exB; }
3423 $html .= '<span class="unchanged">&nbsp;'.$this->formatEntity($glyphs[$p]).$this->formatEntity($SecondGlyph).'</span>';
3424 if ($level==2 && $exL) { $html .= $exL; }
3425 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
3426 if ($level==2 && $exB) { $html .= $exB; }
3427 $html .= '<span class="changed" style="font-feature-settings:\''.$tag.'\' 1;">&nbsp;'.$this->formatEntity($glyphs[$p]).$this->formatEntity($SecondGlyph).'</span>';
3428 if ($level==2 && $exL) { $html .= $exL; }
3429 $html .= ' <span class="unicode">';
3430 if ($Value1['XPlacement']) { $html .= ' Xpl[1]: '.$Value1['XPlacement'].';'; }
3431 if ($Value1['YPlacement']) { $html .= ' YPl[1]: '.$Value1['YPlacement'].';'; }
3432 if ($Value1['XAdvance']) { $html .= ' Xadv[1]: '.$Value1['XAdvance']; }
3433 if ($Value2['XPlacement']) { $html .= ' Xpl[2]: '.$Value2['XPlacement'].';'; }
3434 if ($Value2['YPlacement']) { $html .= ' YPl[2]: '.$Value2['YPlacement'].';'; }
3435 if ($Value2['XAdvance']) { $html .= ' Xadv[2]: '.$Value2['XAdvance']; }
3436 $html .= '</span>';
3437 $html .= '</div>';
3440 $html .= '</div>';
3442 } //===========
3443 // Format 2:
3444 //===========
3445 else {
3446 if ($PosFormat == 2) {
3447 $ClassDef1 = $subtable_offset + $this->read_ushort();
3448 $ClassDef2 = $subtable_offset + $this->read_ushort();
3449 $Class1Count = $this->read_ushort();
3450 $Class2Count = $this->read_ushort();
3452 $sizeOfPair = (2 * $this->count_bits($ValueFormat1)) + (2 * $this->count_bits($ValueFormat2));
3453 $sizeOfValueRecords = $Class1Count * $Class2Count * $sizeOfPair;
3455 // NB Class1Count includes Class 0 even though it is not defined by $ClassDef1
3456 // i.e. Class1Count = 5; Class1 will contain array(indices 1-4);
3457 $Class1 = $this->_getClassDefinitionTable($ClassDef1);
3458 $Class2 = $this->_getClassDefinitionTable($ClassDef2);
3460 $this->seek($subtable_offset + 16);
3462 for ($i = 0; $i < $Class1Count; $i++) {
3463 for ($j = 0; $j < $Class2Count; $j++) {
3464 $Value1 = $this->_getValueRecord($ValueFormat1);
3465 $Value2 = $this->_getValueRecord($ValueFormat2);
3467 // If RTL pairs, GPOS declares a XPlacement e.g. -180 for an XAdvance of -180
3468 // of direction. mPDF does not need the XPlacement adjustment
3469 if ($dir == 'RTL' && $Value1['XPlacement'] && $Value1['XAdvance']) {
3470 $Value1['XPlacement'] -= $Value1['XAdvance'];
3472 if ($ValueFormat2) {
3473 if ($dir == 'RTL' && $Value2['XPlacement'] && $Value2['XAdvance']) {
3474 $Value2['XPlacement'] -= $Value2['XAdvance'];
3478 for ($c1 = 0; $c1 < count($Class1[$i]); $c1++) {
3479 $FirstGlyph = $Class1[$i][$c1];
3480 if ($level == 2 && strpos($lcoverage, $FirstGlyph) === false) {
3481 continue;
3484 for ($c2 = 0; $c2 < count($Class2[$j]); $c2++) {
3485 $SecondGlyph = $Class2[$j][$c2];
3487 if (!$Value1['XPlacement'] && !$Value1['YPlacement'] && !$Value1['XAdvance'] && !$Value2['XPlacement'] && !$Value2['YPlacement'] && !$Value2['XAdvance']) {
3488 continue;
3491 $html .= '<div class="substitution">';
3492 $html .= '<span class="unicode">' . $this->formatUni($FirstGlyph) . '&nbsp;</span> ';
3493 if ($level == 2 && $exB) {
3494 $html .= $exB;
3496 $html .= '<span class="unchanged">&nbsp;' . $this->formatEntity($FirstGlyph) . $this->formatEntity($SecondGlyph) . '</span>';
3497 if ($level == 2 && $exL) {
3498 $html .= $exL;
3500 $html .= '&nbsp; &raquo; &raquo; &nbsp;';
3501 if ($level == 2 && $exB) {
3502 $html .= $exB;
3504 $html .= '<span class="changed" style="font-feature-settings:\'' . $tag . '\' 1;">&nbsp;' . $this->formatEntity($FirstGlyph) . $this->formatEntity($SecondGlyph) . '</span>';
3505 if ($level == 2 && $exL) {
3506 $html .= $exL;
3508 $html .= ' <span class="unicode">';
3509 if ($Value1['XPlacement']) {
3510 $html .= ' Xpl[1]: ' . $Value1['XPlacement'] . ';';
3512 if ($Value1['YPlacement']) {
3513 $html .= ' YPl[1]: ' . $Value1['YPlacement'] . ';';
3515 if ($Value1['XAdvance']) {
3516 $html .= ' Xadv[1]: ' . $Value1['XAdvance'];
3518 if ($Value2['XPlacement']) {
3519 $html .= ' Xpl[2]: ' . $Value2['XPlacement'] . ';';
3521 if ($Value2['YPlacement']) {
3522 $html .= ' YPl[2]: ' . $Value2['YPlacement'] . ';';
3524 if ($Value2['XAdvance']) {
3525 $html .= ' Xadv[2]: ' . $Value2['XAdvance'];
3527 $html .= '</span>';
3528 $html .= '</div>';
3535 } ////////////////////////////////////////////////////////////////////////////////
3536 // LookupType 3: Cursive attachment Attach cursive glyphs
3537 ////////////////////////////////////////////////////////////////////////////////
3538 else {
3539 if ($Lookup[$luli]['Type'] == 3) {
3540 $html .= '<div class="lookuptype">LookupType 3: Cursive attachment </div>';
3541 $Coverage = $subtable_offset + $this->read_ushort();
3542 $EntryExitCount = $this->read_ushort();
3543 $EntryAnchors = [];
3544 $ExitAnchors = [];
3545 for ($i = 0; $i < $EntryExitCount; $i++) {
3546 $EntryAnchors[$i] = $this->read_ushort();
3547 $ExitAnchors[$i] = $this->read_ushort();
3550 $this->seek($Coverage);
3551 $Glyphs = $this->_getCoverage();
3552 for ($i = 0; $i < $EntryExitCount; $i++) {
3553 // Need default XAdvance for glyph
3554 $pdfWidth = $this->mpdf->_getCharWidth($this->mpdf->fonts[$this->fontkey]['cw'], hexdec($Glyphs[$i]));
3555 $EntryAnchor = $EntryAnchors[$i];
3556 $ExitAnchor = $ExitAnchors[$i];
3557 $html .= '<div class="glyphs">';
3558 $html .= '<span class="unchanged">' . $this->formatEntity($Glyphs[$i]) . ' </span> ';
3559 $html .= '<span class="unicode"> ' . $this->formatUni($Glyphs[$i]) . ' => ';
3561 if ($EntryAnchor != 0) {
3562 $EntryAnchor += $subtable_offset;
3563 list($x, $y) = $this->_getAnchorTable($EntryAnchor);
3564 if ($dir == 'RTL') {
3565 if (round($pdfWidth) == round($x * 1000 / $this->mpdf->fonts[$this->fontkey]['desc']['unitsPerEm'])) {
3566 $x = 0;
3567 } else {
3568 $x = $x - ($pdfWidth * $this->mpdf->fonts[$this->fontkey]['desc']['unitsPerEm'] / 1000);
3571 $html .= " Entry X: " . $x . " Y: " . $y . "; ";
3573 if ($ExitAnchor != 0) {
3574 $ExitAnchor += $subtable_offset;
3575 list($x, $y) = $this->_getAnchorTable($ExitAnchor);
3576 if ($dir == 'LTR') {
3577 if (round($pdfWidth) == round($x * 1000 / $this->mpdf->fonts[$this->fontkey]['desc']['unitsPerEm'])) {
3578 $x = 0;
3579 } else {
3580 $x = $x - ($pdfWidth * $this->mpdf->fonts[$this->fontkey]['desc']['unitsPerEm'] / 1000);
3583 $html .= " Exit X: " . $x . " Y: " . $y . "; ";
3586 $html .= '</span></div>';
3588 } ////////////////////////////////////////////////////////////////////////////////
3589 // LookupType 4: MarkToBase attachment Attach a combining mark to a base glyph
3590 ////////////////////////////////////////////////////////////////////////////////
3591 else {
3592 if ($Lookup[$luli]['Type'] == 4) {
3593 $html .= '<div class="lookuptype">LookupType 4: MarkToBase attachment </div>';
3594 $MarkCoverage = $subtable_offset + $this->read_ushort();
3595 $BaseCoverage = $subtable_offset + $this->read_ushort();
3597 $this->seek($MarkCoverage);
3598 $MarkGlyphs = $this->_getCoverage();
3600 $this->seek($BaseCoverage);
3601 $BaseGlyphs = $this->_getCoverage();
3603 $firstMark = '';
3604 $html .= '<div class="glyphs">Marks: ';
3605 for ($i = 0; $i < count($MarkGlyphs); $i++) {
3606 if ($level == 2 && strpos($lcoverage, $MarkGlyphs[$i]) === false) {
3607 continue;
3608 } else {
3609 if (!$firstMark) {
3610 $firstMark = $MarkGlyphs[$i];
3613 $html .= ' ' . $this->formatEntity($MarkGlyphs[$i]) . ' ';
3615 $html .= '</div>';
3616 if (!$firstMark) {
3617 return;
3620 $html .= '<div class="glyphs">Bases: ';
3621 for ($j = 0; $j < count($BaseGlyphs); $j++) {
3622 $html .= ' ' . $this->formatEntity($BaseGlyphs[$j]) . ' ';
3624 $html .= '</div>';
3626 // Example
3627 $html .= '<div class="glyphs" style="font-feature-settings:\'' . $tag . '\' 1;">Example(s): ';
3628 for ($j = 0; $j < min(count($BaseGlyphs), 20); $j++) {
3629 $html .= ' ' . $this->formatEntity($BaseGlyphs[$j]) . $this->formatEntity($firstMark, true) . ' &nbsp; ';
3631 $html .= '</div>';
3632 } ////////////////////////////////////////////////////////////////////////////////
3633 // LookupType 5: MarkToLigature attachment Attach a combining mark to a ligature
3634 ////////////////////////////////////////////////////////////////////////////////
3635 else {
3636 if ($Lookup[$luli]['Type'] == 5) {
3637 $html .= '<div class="lookuptype">LookupType 5: MarkToLigature attachment </div>';
3638 $MarkCoverage = $subtable_offset + $this->read_ushort();
3639 //$MarkCoverage is already set in $lcoverage 00065|00073 etc
3640 $LigatureCoverage = $subtable_offset + $this->read_ushort();
3641 $ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3642 $MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3643 $LigatureArray = $subtable_offset + $this->read_ushort(); // Offset to LigatureArray table
3645 $this->seek($MarkCoverage);
3646 $MarkGlyphs = $this->_getCoverage();
3647 $this->seek($LigatureCoverage);
3648 $LigatureGlyphs = $this->_getCoverage();
3650 $firstMark = '';
3651 $html .= '<div class="glyphs">Marks: <span class="unchanged">';
3652 $MarkRecord = [];
3653 for ($i = 0; $i < count($MarkGlyphs); $i++) {
3654 if ($level == 2 && strpos($lcoverage, $MarkGlyphs[$i]) === false) {
3655 continue;
3656 } else {
3657 if (!$firstMark) {
3658 $firstMark = $MarkGlyphs[$i];
3661 // Get the relevant MarkRecord
3662 $MarkRecord[$i] = $this->_getMarkRecord($MarkArray, $i);
3663 //Mark Class is = $MarkRecord[$i]['Class']
3664 $html .= ' ' . $this->formatEntity($MarkGlyphs[$i]) . ' ';
3666 $html .= '</span></div>';
3667 if (!$firstMark) {
3668 return;
3671 $this->seek($LigatureArray);
3672 $LigatureCount = $this->read_ushort();
3673 $LigatureAttach = [];
3674 $html .= '<div class="glyphs">Ligatures: <span class="unchanged">';
3675 for ($j = 0; $j < count($LigatureGlyphs); $j++) {
3676 // Get the relevant LigatureRecord
3677 $LigatureAttach[$j] = $LigatureArray + $this->read_ushort();
3678 $html .= ' ' . $this->formatEntity($LigatureGlyphs[$j]) . ' ';
3680 $html .= '</span></div>';
3683 for ($i=0;$i<count($MarkGlyphs);$i++) {
3684 $html .= '<div class="glyphs">';
3685 $html .= '<span class="unchanged">'.$this->formatEntity($MarkGlyphs[$i]).'</span>';
3687 for ($j=0;$j<count($LigatureGlyphs);$j++) {
3688 $this->seek($LigatureAttach[$j]);
3689 $ComponentCount = $this->read_ushort();
3690 $html .= '<span class="unchanged">'.$this->formatEntity($LigatureGlyphs[$j]).'</span>';
3691 $offsets = array();
3692 for ($comp=0;$comp<$ComponentCount;$comp++) {
3693 // ComponentRecords
3694 for ($class=0;$class<$ClassCount;$class++) {
3695 $offset = $this->read_ushort();
3696 if ($offset!= 0 && $class == $MarkRecord[$i]['Class']) {
3698 $html .= ' ['.$comp.'] ';
3704 $html .= '</span></div>';
3707 } ////////////////////////////////////////////////////////////////////////////////
3708 // LookupType 6: MarkToMark attachment Attach a combining mark to another mark
3709 ////////////////////////////////////////////////////////////////////////////////
3710 else {
3711 if ($Lookup[$luli]['Type'] == 6) {
3712 $html .= '<div class="lookuptype">LookupType 6: MarkToMark attachment </div>';
3713 $Mark1Coverage = $subtable_offset + $this->read_ushort(); // Combining Mark
3714 //$Mark1Coverage is already set in $LuCoverage 0065|0073 etc
3715 $Mark2Coverage = $subtable_offset + $this->read_ushort(); // Base Mark
3716 $ClassCount = $this->read_ushort(); // Number of classes defined for marks = No. of Combining mark1 glyphs in the MarkCoverage table
3717 $this->seek($Mark1Coverage);
3718 $Mark1Glyphs = $this->_getCoverage();
3719 $this->seek($Mark2Coverage);
3720 $Mark2Glyphs = $this->_getCoverage();
3722 $firstMark = '';
3723 $html .= '<div class="glyphs">Marks: <span class="unchanged">';
3724 for ($i = 0; $i < count($Mark1Glyphs); $i++) {
3725 if ($level == 2 && strpos($lcoverage, $Mark1Glyphs[$i]) === false) {
3726 continue;
3727 } else {
3728 if (!$firstMark) {
3729 $firstMark = $Mark1Glyphs[$i];
3732 $html .= ' ' . $this->formatEntity($Mark1Glyphs[$i]) . ' ';
3734 $html .= '</span></div>';
3736 if ($firstMark) {
3737 $html .= '<div class="glyphs">Bases: <span class="unchanged">';
3738 for ($j = 0; $j < count($Mark2Glyphs); $j++) {
3739 $html .= ' ' . $this->formatEntity($Mark2Glyphs[$j]) . ' ';
3741 $html .= '</span></div>';
3743 // Example
3744 $html .= '<div class="glyphs" style="font-feature-settings:\'' . $tag . '\' 1;">Example(s): <span class="changed">';
3745 for ($j = 0; $j < min(count($Mark2Glyphs), 20); $j++) {
3746 $html .= ' ' . $this->formatEntity($Mark2Glyphs[$j]) . $this->formatEntity($firstMark, true) . ' &nbsp; ';
3748 $html .= '</span></div>';
3750 } ////////////////////////////////////////////////////////////////////////////////
3751 // LookupType 7: Context positioning Position one or more glyphs in context
3752 ////////////////////////////////////////////////////////////////////////////////
3753 else {
3754 if ($Lookup[$luli]['Type'] == 7) {
3755 $html .= '<div class="lookuptype">LookupType 7: Context positioning [Format ' . $PosFormat . ']</div>';
3756 //===========
3757 // Format 1:
3758 //===========
3759 if ($PosFormat == 1) {
3760 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not YET TESTED.");
3761 } //===========
3762 // Format 2:
3763 //===========
3764 else {
3765 if ($PosFormat == 2) {
3766 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not YET TESTED.");
3767 } //===========
3768 // Format 3:
3769 //===========
3770 else {
3771 if ($PosFormat == 3) {
3772 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not YET TESTED.");
3773 } else {
3774 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . ", Format " . $PosFormat . " not supported.");
3778 } ////////////////////////////////////////////////////////////////////////////////
3779 // LookupType 8: Chained Context positioning Position one or more glyphs in chained context
3780 ////////////////////////////////////////////////////////////////////////////////
3781 else {
3782 if ($Lookup[$luli]['Type'] == 8) {
3783 $html .= '<div class="lookuptype">LookupType 8: Chained Context positioning [Format ' . $PosFormat . ']</div>';
3784 //===========
3785 // Format 1:
3786 //===========
3787 if ($PosFormat == 1) {
3788 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3789 } //===========
3790 // Format 2:
3791 //===========
3792 else {
3793 if ($PosFormat == 2) {
3794 $html .= '<div>GPOS Lookup Type 8: Format 2 not yet supported in OTL dump</div>';
3795 continue;
3796 /* NB When developing - cf. GSUB 6.2 */
3797 throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3798 } //===========
3799 // Format 3:
3800 //===========
3801 else {
3802 if ($PosFormat == 3) {
3803 $BacktrackGlyphCount = $this->read_ushort();
3804 $CoverageBacktrackOffset = [];
3805 for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
3806 $CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
3808 $InputGlyphCount = $this->read_ushort();
3809 $CoverageInputOffset = [];
3810 for ($b = 0; $b < $InputGlyphCount; $b++) {
3811 $CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
3813 $LookaheadGlyphCount = $this->read_ushort();
3814 $CoverageLookaheadOffset = [];
3815 for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
3816 $CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
3818 $PosCount = $this->read_ushort();
3820 $PosLookupRecord = [];
3821 for ($p = 0; $p < $PosCount; $p++) {
3822 // PosLookupRecord
3823 $PosLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
3824 $PosLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
3827 $backtrackGlyphs = [];
3828 for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
3829 $this->seek($CoverageBacktrackOffset[$b]);
3830 $backtrackGlyphs[$b] = implode('|', $this->_getCoverage());
3832 $inputGlyphs = [];
3833 for ($b = 0; $b < $InputGlyphCount; $b++) {
3834 $this->seek($CoverageInputOffset[$b]);
3835 $inputGlyphs[$b] = implode('|', $this->_getCoverage());
3837 $lookaheadGlyphs = [];
3838 for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
3839 $this->seek($CoverageLookaheadOffset[$b]);
3840 $lookaheadGlyphs[$b] = implode('|', $this->_getCoverage());
3843 $exampleB = [];
3844 $exampleI = [];
3845 $exampleL = [];
3846 $html .= '<div class="context">CONTEXT: ';
3847 for ($ff = count($backtrackGlyphs) - 1; $ff >= 0; $ff--) {
3848 $html .= '<div>Backtrack #' . $ff . ': <span class="unicode">' . $this->formatUniStr($backtrackGlyphs[$ff]) . '</span></div>';
3849 $exampleB[] = $this->formatEntityFirst($backtrackGlyphs[$ff]);
3851 for ($ff = 0; $ff < count($inputGlyphs); $ff++) {
3852 $html .= '<div>Input #' . $ff . ': <span class="unchanged">&nbsp;' . $this->formatEntityStr($inputGlyphs[$ff]) . '&nbsp;</span></div>';
3853 $exampleI[] = $this->formatEntityFirst($inputGlyphs[$ff]);
3855 for ($ff = 0; $ff < count($lookaheadGlyphs); $ff++) {
3856 $html .= '<div>Lookahead #' . $ff . ': <span class="unicode">' . $this->formatUniStr($lookaheadGlyphs[$ff]) . '</span></div>';
3857 $exampleL[] = $this->formatEntityFirst($lookaheadGlyphs[$ff]);
3859 $html .= '</div>';
3861 for ($p = 0; $p < $PosCount; $p++) {
3862 $lup = $PosLookupRecord[$p]['LookupListIndex'];
3863 $seqIndex = $PosLookupRecord[$p]['SequenceIndex'];
3865 // GENERATE exampleB[n] exampleI[<seqIndex] .... exampleI[>seqIndex] exampleL[n]
3866 $exB = '';
3867 $exL = '';
3868 if (count($exampleB)) {
3869 $exB .= '<span class="backtrack">' . implode('&#x200d;', $exampleB) . '</span>';
3872 if ($seqIndex > 0) {
3873 $exB .= '<span class="inputother">';
3874 for ($ip = 0; $ip < $seqIndex; $ip++) {
3875 $exB .= $exampleI[$ip] . '&#x200d;';
3877 $exB .= '</span>';
3880 if (count($inputGlyphs) > ($seqIndex + 1)) {
3881 $exL .= '<span class="inputother">';
3882 for ($ip = $seqIndex + 1; $ip < count($inputGlyphs); $ip++) {
3883 $exL .= '&#x200d;' . $exampleI[$ip];
3885 $exL .= '</span>';
3888 if (count($exampleL)) {
3889 $exL .= '<span class="lookahead">' . implode('&#x200d;', $exampleL) . '</span>';
3892 $html .= '<div class="sequenceIndex">Substitution Position: ' . $seqIndex . '</div>';
3894 $lul2 = [$lup => $tag];
3896 // Only apply if the (first) 'Replace' glyph from the
3897 // Lookup list is in the [inputGlyphs] at ['SequenceIndex']
3898 // Pass $inputGlyphs[$seqIndex] e.g. 00636|00645|00656
3899 // to level 2 and only apply if first Replace glyph is in this list
3900 $html .= $this->_getGPOSarray($Lookup, $lul2, $scripttag, 2, $inputGlyphs[$seqIndex], $exB, $exL);
3914 $html .= '</div>';
3916 if ($level == 1) {
3917 $this->mpdf->WriteHTML($html);
3918 } else {
3919 return $html;
3921 //print_r($Lookup); exit;
3924 //=====================================================================================
3925 //=====================================================================================
3926 // GPOS FUNCTIONS
3927 //=====================================================================================
3929 function count_bits($n)
3931 for ($c = 0; $n; $c++) {
3932 $n &= $n - 1; // clear the least significant bit set
3935 return $c;
3938 function _getValueRecord($ValueFormat)
3940 // Common ValueRecord for GPOS
3941 // Only returns 3 possible: $vra['XPlacement'] $vra['YPlacement'] $vra['XAdvance']
3942 $vra = [];
3943 // Horizontal adjustment for placement-in design units
3944 if (($ValueFormat & 0x0001) == 0x0001) {
3945 $vra['XPlacement'] = $this->read_short();
3947 // Vertical adjustment for placement-in design units
3948 if (($ValueFormat & 0x0002) == 0x0002) {
3949 $vra['YPlacement'] = $this->read_short();
3951 // Horizontal adjustment for advance-in design units (only used for horizontal writing)
3952 if (($ValueFormat & 0x0004) == 0x0004) {
3953 $vra['XAdvance'] = $this->read_short();
3955 // Vertical adjustment for advance-in design units (only used for vertical writing)
3956 if (($ValueFormat & 0x0008) == 0x0008) {
3957 $this->read_short();
3959 // Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)
3960 if (($ValueFormat & 0x0010) == 0x0010) {
3961 $this->read_ushort();
3963 // Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)
3964 if (($ValueFormat & 0x0020) == 0x0020) {
3965 $this->read_ushort();
3967 // Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)
3968 if (($ValueFormat & 0x0040) == 0x0040) {
3969 $this->read_ushort();
3971 // Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)
3972 if (($ValueFormat & 0x0080) == 0x0080) {
3973 $this->read_ushort();
3976 return $vra;
3979 function _getAnchorTable($offset = 0)
3981 if ($offset) {
3982 $this->seek($offset);
3984 $AnchorFormat = $this->read_ushort();
3985 $XCoordinate = $this->read_short();
3986 $YCoordinate = $this->read_short();
3988 // Format 2 specifies additional link to contour point; Format 3 additional Device table
3989 return [$XCoordinate, $YCoordinate];
3992 function _getMarkRecord($offset, $MarkPos)
3994 $this->seek($offset);
3995 $MarkCount = $this->read_ushort();
3996 $this->skip($MarkPos * 4);
3997 $Class = $this->read_ushort();
3998 $MarkAnchor = $offset + $this->read_ushort(); // = Offset to anchor table
3999 list($x, $y) = $this->_getAnchorTable($MarkAnchor);
4000 $MarkRecord = ['Class' => $Class, 'AnchorX' => $x, 'AnchorY' => $y];
4002 return $MarkRecord;
4005 //////////////////////////////////////////////////////////////////////////////////
4006 // Recursively get composite glyph data
4007 function getGlyphData($originalGlyphIdx, &$maxdepth, &$depth, &$points, &$contours)
4009 $depth++;
4010 $maxdepth = max($maxdepth, $depth);
4011 if (count($this->glyphdata[$originalGlyphIdx]['compGlyphs'])) {
4012 foreach ($this->glyphdata[$originalGlyphIdx]['compGlyphs'] as $glyphIdx) {
4013 $this->getGlyphData($glyphIdx, $maxdepth, $depth, $points, $contours);
4015 } else {
4016 if (($this->glyphdata[$originalGlyphIdx]['nContours'] > 0) && $depth > 0) { // simple
4017 $contours += $this->glyphdata[$originalGlyphIdx]['nContours'];
4018 $points += $this->glyphdata[$originalGlyphIdx]['nPoints'];
4021 $depth--;
4024 //////////////////////////////////////////////////////////////////////////////////
4025 // Recursively get composite glyphs
4026 function getGlyphs($originalGlyphIdx, &$start, &$glyphSet, &$subsetglyphs)
4028 $glyphPos = $this->glyphPos[$originalGlyphIdx];
4029 $glyphLen = $this->glyphPos[$originalGlyphIdx + 1] - $glyphPos;
4030 if (!$glyphLen) {
4031 return;
4033 $this->seek($start + $glyphPos);
4034 $numberOfContours = $this->read_short();
4035 if ($numberOfContours < 0) {
4036 $this->skip(8);
4037 $flags = GlyphOperator::MORE;
4038 while ($flags & GlyphOperator::MORE) {
4039 $flags = $this->read_ushort();
4041 $glyphIdx = $this->read_ushort();
4042 if (!isset($glyphSet[$glyphIdx])) {
4043 $glyphSet[$glyphIdx] = count($subsetglyphs); // old glyphID to new glyphID
4044 $subsetglyphs[$glyphIdx] = true;
4046 $savepos = ftell($this->fh);
4047 $this->getGlyphs($glyphIdx, $start, $glyphSet, $subsetglyphs);
4048 $this->seek($savepos);
4049 if ($flags & GlyphOperator::WORDS) {
4050 $this->skip(4);
4051 } else {
4052 $this->skip(2);
4054 if ($flags & GlyphOperator::SCALE) {
4055 $this->skip(2);
4056 } else {
4057 if ($flags & GlyphOperator::XYSCALE) {
4058 $this->skip(4);
4059 } else {
4060 if ($flags & GlyphOperator::TWOBYTWO) {
4061 $this->skip(8);
4069 //////////////////////////////////////////////////////////////////////////////////
4071 function getHMTX($numberOfHMetrics, $numGlyphs, &$glyphToChar, $scale)
4073 $start = $this->seek_table("hmtx");
4074 $aw = 0;
4075 $this->charWidths = str_pad('', 256 * 256 * 2, "\x00");
4076 if ($this->maxUniChar > 65536) {
4077 $this->charWidths .= str_pad('', 256 * 256 * 2, "\x00");
4078 } // Plane 1 SMP
4079 if ($this->maxUniChar > 131072) {
4080 $this->charWidths .= str_pad('', 256 * 256 * 2, "\x00");
4081 } // Plane 2 SMP
4082 $nCharWidths = 0;
4083 if (($numberOfHMetrics * 4) < $this->maxStrLenRead) {
4084 $data = $this->get_chunk($start, ($numberOfHMetrics * 4));
4085 $arr = unpack("n*", $data);
4086 } else {
4087 $this->seek($start);
4089 for ($glyph = 0; $glyph < $numberOfHMetrics; $glyph++) {
4090 if (($numberOfHMetrics * 4) < $this->maxStrLenRead) {
4091 $aw = $arr[($glyph * 2) + 1];
4092 } else {
4093 $aw = $this->read_ushort();
4094 $lsb = $this->read_ushort();
4096 if (isset($glyphToChar[$glyph]) || $glyph == 0) {
4097 if ($aw >= (1 << 15)) {
4098 $aw = 0;
4099 } // 1.03 Some (arabic) fonts have -ve values for width
4100 // although should be unsigned value - comes out as e.g. 65108 (intended -50)
4101 if ($glyph == 0) {
4102 $this->defaultWidth = $scale * $aw;
4103 continue;
4105 foreach ($glyphToChar[$glyph] as $char) {
4106 //$this->charWidths[$char] = intval(round($scale*$aw));
4107 if ($char != 0 && $char != 65535) {
4108 $w = intval(round($scale * $aw));
4109 if ($w == 0) {
4110 $w = 65535;
4112 if ($char < 196608) {
4113 $this->charWidths[$char * 2] = chr($w >> 8);
4114 $this->charWidths[$char * 2 + 1] = chr($w & 0xFF);
4115 $nCharWidths++;
4121 $data = $this->get_chunk(($start + $numberOfHMetrics * 4), ($numGlyphs * 2));
4122 $arr = unpack("n*", $data);
4123 $diff = $numGlyphs - $numberOfHMetrics;
4124 $w = intval(round($scale * $aw));
4125 if ($w == 0) {
4126 $w = 65535;
4128 for ($pos = 0; $pos < $diff; $pos++) {
4129 $glyph = $pos + $numberOfHMetrics;
4130 if (isset($glyphToChar[$glyph])) {
4131 foreach ($glyphToChar[$glyph] as $char) {
4132 if ($char != 0 && $char != 65535) {
4133 if ($char < 196608) {
4134 $this->charWidths[$char * 2] = chr($w >> 8);
4135 $this->charWidths[$char * 2 + 1] = chr($w & 0xFF);
4136 $nCharWidths++;
4142 // NB 65535 is a set width of 0
4143 // First bytes define number of chars in font
4144 $this->charWidths[0] = chr($nCharWidths >> 8);
4145 $this->charWidths[1] = chr($nCharWidths & 0xFF);
4148 function getHMetric($numberOfHMetrics, $gid)
4150 $start = $this->seek_table("hmtx");
4151 if ($gid < $numberOfHMetrics) {
4152 $this->seek($start + ($gid * 4));
4153 $hm = fread($this->fh, 4);
4154 } else {
4155 $this->seek($start + (($numberOfHMetrics - 1) * 4));
4156 $hm = fread($this->fh, 2);
4157 $this->seek($start + ($numberOfHMetrics * 2) + ($gid * 2));
4158 $hm .= fread($this->fh, 2);
4161 return $hm;
4164 function getLOCA($indexToLocFormat, $numGlyphs)
4166 $start = $this->seek_table('loca');
4167 $this->glyphPos = [];
4168 if ($indexToLocFormat == 0) {
4169 $data = $this->get_chunk($start, ($numGlyphs * 2) + 2);
4170 $arr = unpack("n*", $data);
4171 for ($n = 0; $n <= $numGlyphs; $n++) {
4172 $this->glyphPos[] = ($arr[$n + 1] * 2);
4174 } else {
4175 if ($indexToLocFormat == 1) {
4176 $data = $this->get_chunk($start, ($numGlyphs * 4) + 4);
4177 $arr = unpack("N*", $data);
4178 for ($n = 0; $n <= $numGlyphs; $n++) {
4179 $this->glyphPos[] = ($arr[$n + 1]);
4181 } else {
4182 throw new \Mpdf\MpdfException('Unknown location table format ' . $indexToLocFormat);
4187 // CMAP Format 4
4188 function getCMAP4($unicode_cmap_offset, &$glyphToChar, &$charToGlyph)
4190 $this->maxUniChar = 0;
4191 $this->seek($unicode_cmap_offset + 2);
4192 $length = $this->read_ushort();
4193 $limit = $unicode_cmap_offset + $length;
4194 $this->skip(2);
4196 $segCount = $this->read_ushort() / 2;
4197 $this->skip(6);
4198 $endCount = [];
4199 for ($i = 0; $i < $segCount; $i++) {
4200 $endCount[] = $this->read_ushort();
4202 $this->skip(2);
4203 $startCount = [];
4204 for ($i = 0; $i < $segCount; $i++) {
4205 $startCount[] = $this->read_ushort();
4207 $idDelta = [];
4208 for ($i = 0; $i < $segCount; $i++) {
4209 $idDelta[] = $this->read_short();
4210 } // ???? was unsigned short
4211 $idRangeOffset_start = $this->_pos;
4212 $idRangeOffset = [];
4213 for ($i = 0; $i < $segCount; $i++) {
4214 $idRangeOffset[] = $this->read_ushort();
4217 for ($n = 0; $n < $segCount; $n++) {
4218 $endpoint = ($endCount[$n] + 1);
4219 for ($unichar = $startCount[$n]; $unichar < $endpoint; $unichar++) {
4220 if ($idRangeOffset[$n] == 0) {
4221 $glyph = ($unichar + $idDelta[$n]) & 0xFFFF;
4222 } else {
4223 $offset = ($unichar - $startCount[$n]) * 2 + $idRangeOffset[$n];
4224 $offset = $idRangeOffset_start + 2 * $n + $offset;
4225 if ($offset >= $limit) {
4226 $glyph = 0;
4227 } else {
4228 $glyph = $this->get_ushort($offset);
4229 if ($glyph != 0) {
4230 $glyph = ($glyph + $idDelta[$n]) & 0xFFFF;
4234 $charToGlyph[$unichar] = $glyph;
4235 if ($unichar < 196608) {
4236 $this->maxUniChar = max($unichar, $this->maxUniChar);
4238 $glyphToChar[$glyph][] = $unichar;
4243 function formatUni($char)
4245 $x = preg_replace('/^[0]*/', '', $char);
4246 $x = str_pad($x, 4, '0', STR_PAD_LEFT);
4247 $d = hexdec($x);
4248 if (($d > 57343 && $d < 63744) || ($d > 122879 && $d < 126977)) {
4249 $id = 'M';
4250 } // E000 - F8FF, 1E000-1F000
4251 else {
4252 $id = 'U';
4255 return $id . '+' . $x;
4258 function formatEntity($char, $allowjoining = false)
4260 $char = preg_replace('/^[0]/', '', $char);
4261 $x = '&#x' . $char . ';';
4262 if (strpos($this->GlyphClassMarks, $char) !== false) {
4263 if (!$allowjoining) {
4264 $x = '&#x25cc;' . $x;
4268 return $x;
4271 function formatUniArr($arr)
4273 $s = [];
4274 foreach ($arr as $c) {
4275 $x = preg_replace('/^[0]*/', '', $c);
4276 $d = hexdec($x);
4277 if (($d > 57343 && $d < 63744) || ($d > 122879 && $d < 126977)) {
4278 $id = 'M';
4279 } // E000 - F8FF, 1E000-1F000
4280 else {
4281 $id = 'U';
4283 $s[] = $id . '+' . str_pad($x, 4, '0', STR_PAD_LEFT);
4286 return implode(', ', $s);
4289 function formatEntityArr($arr)
4291 $s = [];
4292 foreach ($arr as $c) {
4293 $c = preg_replace('/^[0]/', '', $c);
4294 $x = '&#x' . $c . ';';
4295 if (strpos($this->GlyphClassMarks, $c) !== false) {
4296 $x = '&#x25cc;' . $x;
4298 $s[] = $x;
4301 return implode(' ', $s); // ZWNJ? &#x200d;
4304 function formatClassArr($arr)
4306 $s = [];
4307 foreach ($arr as $c) {
4308 $x = preg_replace('/^[0]*/', '', $c);
4309 $d = hexdec($x);
4310 if (($d > 57343 && $d < 63744) || ($d > 122879 && $d < 126977)) {
4311 $id = 'M';
4312 } // E000 - F8FF, 1E000-1F000
4313 else {
4314 $id = 'U';
4316 $s[] = $id . '+' . str_pad($x, 4, '0', STR_PAD_LEFT);
4319 return implode(', ', $s);
4322 function formatUniStr($str)
4324 $s = [];
4325 $arr = explode('|', $str);
4326 foreach ($arr as $c) {
4327 $x = preg_replace('/^[0]*/', '', $c);
4328 $d = hexdec($x);
4329 if (($d > 57343 && $d < 63744) || ($d > 122879 && $d < 126977)) {
4330 $id = 'M';
4331 } // E000 - F8FF, 1E000-1F000
4332 else {
4333 $id = 'U';
4335 $s[] = $id . '+' . str_pad($x, 4, '0', STR_PAD_LEFT);
4338 return implode(', ', $s);
4341 function formatEntityStr($str)
4343 $s = [];
4344 $arr = explode('|', $str);
4345 foreach ($arr as $c) {
4346 $c = preg_replace('/^[0]/', '', $c);
4347 $x = '&#x' . $c . ';';
4348 if (strpos($this->GlyphClassMarks, $c) !== false) {
4349 $x = '&#x25cc;' . $x;
4351 $s[] = $x;
4354 return implode(' ', $s); // ZWNJ? &#x200d;
4357 function formatEntityFirst($str)
4359 $arr = explode('|', $str);
4360 $char = preg_replace('/^[0]/', '', $arr[0]);
4361 $x = '&#x' . $char . ';';
4362 if (strpos($this->GlyphClassMarks, $char) !== false) {
4363 $x = '&#x25cc;' . $x;
4366 return $x;