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