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