1<?php
2
3namespace Mpdf;
4
5use Mpdf\Strict;
6
7use Mpdf\Css\TextVars;
8use Mpdf\Fonts\FontCache;
9
10use Mpdf\Shaper\Indic;
11use Mpdf\Shaper\Myanmar;
12use Mpdf\Shaper\Sea;
13
14use Mpdf\Utils\UtfString;
15
16class Otl
17{
18
19	use Strict;
20
21	const _OTL_OLD_SPEC_COMPAT_1 = true;
22	const _DICT_NODE_TYPE_SPLIT = 0x01;
23	const _DICT_NODE_TYPE_LINEAR = 0x02;
24	const _DICT_INTERMEDIATE_MATCH = 0x03;
25	const _DICT_FINAL_MATCH = 0x04;
26
27	private $mpdf;
28
29	private $fontCache;
30
31	var $arabLeftJoining;
32
33	var $arabRightJoining;
34
35	var $arabTransparentJoin;
36
37	var $arabTransparent;
38
39	var $GSUBdata;
40
41	var $GPOSdata;
42
43	var $GSUBfont;
44
45	var $fontkey;
46
47	var $ttfOTLdata;
48
49	var $glyphIDtoUni;
50
51	var $_pos;
52
53	var $GSUB_offset;
54
55	var $GPOS_offset;
56
57	var $MarkAttachmentType;
58
59	var $MarkGlyphSets;
60
61	var $GlyphClassMarks;
62
63	var $GlyphClassLigatures;
64
65	var $GlyphClassBases;
66
67	var $GlyphClassComponents;
68
69	var $Ignores;
70
71	var $LuCoverage;
72
73	var $OTLdata;
74
75	var $assocLigs;
76
77	var $assocMarks;
78
79	var $shaper;
80
81	var $restrictToSyllable;
82
83	var $lbdicts; // Line-breaking dictionaries
84
85	var $LuDataCache;
86
87	var $arabGlyphs;
88
89	var $current_fh;
90
91	var $Entry;
92
93	var $Exit;
94
95	var $GDEFdata;
96
97	var $GPOSLookups;
98
99	var $GSLuCoverage;
100
101	var $GSUB_length;
102
103	var $GSUBLookups;
104
105	var $schOTLdata;
106
107	var $lastBidiStrongType;
108
109	var $debugOTL = false;
110
111	public function __construct(Mpdf $mpdf, FontCache $fontCache)
112	{
113		$this->mpdf = $mpdf;
114		$this->fontCache = $fontCache;
115
116		$this->current_fh = '';
117
118		$this->lbdicts = [];
119		$this->LuDataCache = [];
120	}
121
122	function applyOTL($str, $useOTL)
123	{
124		if (!$this->arabLeftJoining) {
125			$this->arabic_initialise();
126		}
127
128		$this->OTLdata = [];
129		if (trim($str) == '') {
130			return $str;
131		}
132		if (!$useOTL) {
133			return $str;
134		}
135
136		// 1. Load GDEF data
137		//==============================
138		$this->fontkey = $this->mpdf->CurrentFont['fontkey'];
139		$this->glyphIDtoUni = $this->mpdf->CurrentFont['glyphIDtoUni'];
140		$fontCacheFilename = $this->fontkey . '.GDEFdata.json';
141		if (!isset($this->GDEFdata[$this->fontkey]) && $this->fontCache->jsonHas($fontCacheFilename)) {
142			$font = $this->fontCache->jsonLoad($fontCacheFilename);
143
144			$this->GSUB_offset = $this->GDEFdata[$this->fontkey]['GSUB_offset'] = $font['GSUB_offset'];
145			$this->GPOS_offset = $this->GDEFdata[$this->fontkey]['GPOS_offset'] = $font['GPOS_offset'];
146			$this->GSUB_length = $this->GDEFdata[$this->fontkey]['GSUB_length'] = $font['GSUB_length'];
147			$this->MarkAttachmentType = $this->GDEFdata[$this->fontkey]['MarkAttachmentType'] = $font['MarkAttachmentType'];
148			$this->MarkGlyphSets = $this->GDEFdata[$this->fontkey]['MarkGlyphSets'] = $font['MarkGlyphSets'];
149			$this->GlyphClassMarks = $this->GDEFdata[$this->fontkey]['GlyphClassMarks'] = $font['GlyphClassMarks'];
150			$this->GlyphClassLigatures = $this->GDEFdata[$this->fontkey]['GlyphClassLigatures'] = $font['GlyphClassLigatures'];
151			$this->GlyphClassComponents = $this->GDEFdata[$this->fontkey]['GlyphClassComponents'] = $font['GlyphClassComponents'];
152			$this->GlyphClassBases = $this->GDEFdata[$this->fontkey]['GlyphClassBases'] = $font['GlyphClassBases'];
153		} else {
154			$this->GSUB_offset = $this->GDEFdata[$this->fontkey]['GSUB_offset'];
155			$this->GPOS_offset = $this->GDEFdata[$this->fontkey]['GPOS_offset'];
156			$this->GSUB_length = $this->GDEFdata[$this->fontkey]['GSUB_length'];
157			$this->MarkAttachmentType = $this->GDEFdata[$this->fontkey]['MarkAttachmentType'];
158			$this->MarkGlyphSets = $this->GDEFdata[$this->fontkey]['MarkGlyphSets'];
159			$this->GlyphClassMarks = $this->GDEFdata[$this->fontkey]['GlyphClassMarks'];
160			$this->GlyphClassLigatures = $this->GDEFdata[$this->fontkey]['GlyphClassLigatures'];
161			$this->GlyphClassComponents = $this->GDEFdata[$this->fontkey]['GlyphClassComponents'];
162			$this->GlyphClassBases = $this->GDEFdata[$this->fontkey]['GlyphClassBases'];
163		}
164
165		// 2. Prepare string as HEX string and Analyse character properties
166		//=================================================================
167		$earr = $this->mpdf->UTF8StringToArray($str, false);
168
169		$scriptblock = 0;
170		$scriptblocks = [];
171		$scriptblocks[0] = 0;
172		$vstr = '';
173		$OTLdata = [];
174		$subchunk = 0;
175		$charctr = 0;
176		foreach ($earr as $char) {
177			$ucd_record = Ucdn::get_ucd_record($char);
178			$sbl = $ucd_record[6];
179
180			// Special case - Arabic End of Ayah
181			if ($char == 1757) {
182				$sbl = Ucdn::SCRIPT_ARABIC;
183			}
184
185			if ($sbl && $sbl != 40 && $sbl != 102) {
186				if ($scriptblock == 0) {
187					$scriptblock = $sbl;
188					$scriptblocks[$subchunk] = $scriptblock;
189				} elseif ($scriptblock > 0 && $scriptblock != $sbl) {
190					// *************************************************
191					// NEW (non-common) Script encountered in this chunk. Start a new subchunk
192					$subchunk++;
193					$scriptblock = $sbl;
194					$charctr = 0;
195					$scriptblocks[$subchunk] = $scriptblock;
196				}
197			}
198
199			$OTLdata[$subchunk][$charctr]['general_category'] = $ucd_record[0];
200			$OTLdata[$subchunk][$charctr]['bidi_type'] = $ucd_record[2];
201
202			//$OTLdata[$subchunk][$charctr]['combining_class'] = $ucd_record[1];
203			//$OTLdata[$subchunk][$charctr]['bidi_type'] = $ucd_record[2];
204			//$OTLdata[$subchunk][$charctr]['mirrored'] = $ucd_record[3];
205			//$OTLdata[$subchunk][$charctr]['east_asian_width'] = $ucd_record[4];
206			//$OTLdata[$subchunk][$charctr]['normalization_check'] = $ucd_record[5];
207			//$OTLdata[$subchunk][$charctr]['script'] = $ucd_record[6];
208
209			$charasstr = $this->unicode_hex($char);
210
211			if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
212				$OTLdata[$subchunk][$charctr]['group'] = 'M';
213			} elseif ($char == 32 || $char == 12288) {
214				$OTLdata[$subchunk][$charctr]['group'] = 'S';
215			} // 12288 = 0x3000 = CJK space
216			else {
217				$OTLdata[$subchunk][$charctr]['group'] = 'C';
218			}
219
220			$OTLdata[$subchunk][$charctr]['uni'] = $char;
221			$OTLdata[$subchunk][$charctr]['hex'] = $charasstr;
222			$charctr++;
223		}
224
225		/* PROCESS EACH SUBCHUNK WITH DIFFERENT SCRIPTS */
226		for ($sch = 0; $sch <= $subchunk; $sch++) {
227			$this->OTLdata = $OTLdata[$sch];
228			$scriptblock = $scriptblocks[$sch];
229
230			// 3. Get Appropriate Scripts, and Shaper engine from analysing text and list of available scripts/langsys in font
231			//==============================
232			// Based on actual script block of text, select shaper (and line-breaking dictionaries)
233			if (Ucdn::SCRIPT_DEVANAGARI <= $scriptblock && $scriptblock <= Ucdn::SCRIPT_MALAYALAM) {
234				$this->shaper = "I";
235			} // INDIC shaper
236			elseif ($scriptblock == Ucdn::SCRIPT_ARABIC || $scriptblock == Ucdn::SCRIPT_SYRIAC) {
237				$this->shaper = "A";
238			} // ARABIC shaper
239			elseif ($scriptblock == Ucdn::SCRIPT_NKO || $scriptblock == Ucdn::SCRIPT_MANDAIC) {
240				$this->shaper = "A";
241			} // ARABIC shaper
242			elseif ($scriptblock == Ucdn::SCRIPT_KHMER) {
243				$this->shaper = "K";
244			} // KHMER shaper
245			elseif ($scriptblock == Ucdn::SCRIPT_THAI) {
246				$this->shaper = "T";
247			} // THAI shaper
248			elseif ($scriptblock == Ucdn::SCRIPT_LAO) {
249				$this->shaper = "L";
250			} // LAO shaper
251			elseif ($scriptblock == Ucdn::SCRIPT_SINHALA) {
252				$this->shaper = "S";
253			} // SINHALA shaper
254			elseif ($scriptblock == Ucdn::SCRIPT_MYANMAR) {
255				$this->shaper = "M";
256			} // MYANMAR shaper
257			elseif ($scriptblock == Ucdn::SCRIPT_NEW_TAI_LUE) {
258				$this->shaper = "E";
259			} // SEA South East Asian shaper
260			elseif ($scriptblock == Ucdn::SCRIPT_CHAM) {
261				$this->shaper = "E";
262			} // SEA South East Asian shaper
263			elseif ($scriptblock == Ucdn::SCRIPT_TAI_THAM) {
264				$this->shaper = "E";
265			} // SEA South East Asian shaper
266			else {
267				$this->shaper = "";
268			}
269			// Get scripttag based on actual text script
270			$scripttag = Ucdn::$uni_scriptblock[$scriptblock];
271
272			$GSUBscriptTag = '';
273			$GSUBlangsys = '';
274			$GPOSscriptTag = '';
275			$GPOSlangsys = '';
276			$is_old_spec = false;
277
278			$ScriptLang = $this->mpdf->CurrentFont['GSUBScriptLang'];
279			if (count($ScriptLang)) {
280				list($GSUBscriptTag, $is_old_spec) = $this->_getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $this->shaper, $useOTL, 'GSUB');
281				if ($this->mpdf->fontLanguageOverride && strpos($ScriptLang[$GSUBscriptTag], $this->mpdf->fontLanguageOverride) !== false) {
282					$GSUBlangsys = str_pad($this->mpdf->fontLanguageOverride, 4);
283				} elseif ($GSUBscriptTag && isset($ScriptLang[$GSUBscriptTag]) && $ScriptLang[$GSUBscriptTag] != '') {
284					$GSUBlangsys = $this->_getOTLLangTag($this->mpdf->currentLang, $ScriptLang[$GSUBscriptTag]);
285				}
286			}
287			$ScriptLang = $this->mpdf->CurrentFont['GPOSScriptLang'];
288
289			// NB If after GSUB, the same script/lang exist for GPOS, just use these...
290			if ($GSUBscriptTag && $GSUBlangsys && isset($ScriptLang[$GSUBscriptTag]) && strpos($ScriptLang[$GSUBscriptTag], $GSUBlangsys) !== false) {
291				$GPOSlangsys = $GSUBlangsys;
292				$GPOSscriptTag = $GSUBscriptTag;
293			} // else repeat for GPOS
294			// [Font XBRiyaz has GSUB tables for latn, but not GPOS for latn]
295			elseif (count($ScriptLang)) {
296				list($GPOSscriptTag, $dummy) = $this->_getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $this->shaper, $useOTL, 'GPOS');
297				if ($GPOSscriptTag && $this->mpdf->fontLanguageOverride && strpos($ScriptLang[$GPOSscriptTag], $this->mpdf->fontLanguageOverride) !== false) {
298					$GPOSlangsys = str_pad($this->mpdf->fontLanguageOverride, 4);
299				} elseif ($GPOSscriptTag && isset($ScriptLang[$GPOSscriptTag]) && $ScriptLang[$GPOSscriptTag] != '') {
300					$GPOSlangsys = $this->_getOTLLangTag($this->mpdf->currentLang, $ScriptLang[$GPOSscriptTag]);
301				}
302			}
303
304			// This is just for the font_dump_OTL utility to set script and langsys override
305			// $mpdf->overrideOTLsettings does not exist, this is never called
306			/*if (isset($this->mpdf->overrideOTLsettings) && isset($this->mpdf->overrideOTLsettings[$this->fontkey])) {
307				$GSUBscriptTag = $GPOSscriptTag = $this->mpdf->overrideOTLsettings[$this->fontkey]['script'];
308				$GSUBlangsys = $GPOSlangsys = $this->mpdf->overrideOTLsettings[$this->fontkey]['lang'];
309			}*/
310
311			if (!$GSUBscriptTag && !$GSUBlangsys && !$GPOSscriptTag && !$GPOSlangsys) {
312				// Remove ZWJ and ZWNJ
313				for ($i = 0; $i < count($this->OTLdata); $i++) {
314					if ($this->OTLdata[$i]['uni'] == 8204 || $this->OTLdata[$i]['uni'] == 8205) {
315						array_splice($this->OTLdata, $i, 1);
316					}
317				}
318				$this->schOTLdata[$sch] = $this->OTLdata;
319				$this->OTLdata = [];
320				continue;
321			}
322
323			// Don't use MYANMAR shaper unless using v2 scripttag
324			if ($this->shaper == 'M' && $GSUBscriptTag != 'mym2') {
325				$this->shaper = '';
326			}
327
328			$GSUBFeatures = (isset($this->mpdf->CurrentFont['GSUBFeatures'][$GSUBscriptTag][$GSUBlangsys]) ? $this->mpdf->CurrentFont['GSUBFeatures'][$GSUBscriptTag][$GSUBlangsys] : false);
329			$GPOSFeatures = (isset($this->mpdf->CurrentFont['GPOSFeatures'][$GPOSscriptTag][$GPOSlangsys]) ? $this->mpdf->CurrentFont['GPOSFeatures'][$GPOSscriptTag][$GPOSlangsys] : false);
330
331			$this->assocLigs = []; // Ligatures[$posarr lpos] => nc
332			$this->assocMarks = [];  // assocMarks[$posarr mpos] => array(compID, ligPos)
333
334			if (!isset($this->GDEFdata[$this->fontkey]['GSUBGPOStables'])) {
335				$this->ttfOTLdata = $this->GDEFdata[$this->fontkey]['GSUBGPOStables'] = $this->fontCache->load($this->fontkey . '.GSUBGPOStables.dat', 'rb');
336				if (!$this->ttfOTLdata) {
337					throw new \Mpdf\MpdfException('Can\'t open file ' . $this->fontCache->tempFilename($this->fontkey . '.GSUBGPOStables.dat'));
338				}
339			} else {
340				$this->ttfOTLdata = $this->GDEFdata[$this->fontkey]['GSUBGPOStables'];
341			}
342
343			if ($this->debugOTL) {
344				$this->_dumpproc('BEGIN', '-', '-', '-', '-', -1, '-', 0);
345			}
346
347			////////////////////////////////////////////////////////////////
348			/////////  LINE BREAKING FOR KHMER, THAI + LAO /////////////////
349			////////////////////////////////////////////////////////////////
350			// Insert U+200B at word boundaries using dictionaries
351			if ($this->mpdf->useDictionaryLBR && ($this->shaper == "K" || $this->shaper == "T" || $this->shaper == "L")) {
352				// Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
353				$this->seaLineBreaking();
354			} // Insert U+200B at word boundaries for Tibetan
355			elseif ($this->mpdf->useTibetanLBR && $scriptblock == Ucdn::SCRIPT_TIBETAN) {
356				// Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
357				$this->tibetanLineBreaking();
358			}
359
360
361			////////////////////////////////////////////////////////////////
362			//////////       GSUB          /////////////////////////////////
363			////////////////////////////////////////////////////////////////
364			if (($useOTL & 0xFF) && $GSUBscriptTag && $GSUBlangsys && $GSUBFeatures) {
365				// 4. Load GSUB data, Coverage & Lookups
366				//=================================================================
367
368				$this->GSUBfont = $this->fontkey . '.GSUB.' . $GSUBscriptTag . '.' . $GSUBlangsys;
369
370				if (!isset($this->GSUBdata[$this->GSUBfont])) {
371					$fontCacheFilename = $this->GSUBfont . '.json';
372					if ($this->fontCache->jsonHas($fontCacheFilename)) {
373						$font = $this->fontCache->jsonLoad($fontCacheFilename);
374
375						$this->GSUBdata[$this->GSUBfont]['rtlSUB'] = $font['rtlSUB'];
376						$this->GSUBdata[$this->GSUBfont]['finals'] = $font['finals'];
377						if ($this->shaper == 'I') {
378							$this->GSUBdata[$this->GSUBfont]['rphf'] = $font['rphf'];
379							$this->GSUBdata[$this->GSUBfont]['half'] = $font['half'];
380							$this->GSUBdata[$this->GSUBfont]['pref'] = $font['pref'];
381							$this->GSUBdata[$this->GSUBfont]['blwf'] = $font['blwf'];
382							$this->GSUBdata[$this->GSUBfont]['pstf'] = $font['pstf'];
383						}
384					} else {
385						$this->GSUBdata[$this->GSUBfont] = ['rtlSUB' => [], 'rphf' => [], 'rphf' => [],
386							'pref' => [], 'blwf' => [], 'pstf' => [], 'finals' => ''
387						];
388					}
389				}
390
391				$fontCacheFilename = $this->fontkey . '.GSUBdata.json';
392				if (!isset($this->GSUBdata[$this->fontkey]) && $this->fontCache->jsonHas($fontCacheFilename)) {
393					$this->GSLuCoverage = $this->GSUBdata[$this->fontkey]['GSLuCoverage'] = $this->fontCache->jsonLoad($fontCacheFilename);
394				} else {
395					$this->GSLuCoverage = $this->GSUBdata[$this->fontkey]['GSLuCoverage'];
396				}
397
398				$this->GSUBLookups = $this->mpdf->CurrentFont['GSUBLookups'];
399
400
401				// 5(A). GSUB - Shaper - ARABIC
402				//==============================
403				if ($this->shaper == 'A') {
404					//-----------------------------------------------------------------------------------
405					// a. Apply initial GSUB Lookups (in order specified in lookup list but only selecting from certain tags)
406					//-----------------------------------------------------------------------------------
407					$tags = 'locl ccmp';
408					$omittags = '';
409					$usetags = $tags;
410					if (!empty($this->mpdf->OTLtags)) {
411						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, true);
412					}
413					$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
414
415					//-----------------------------------------------------------------------------------
416					// b. Apply context-specific forms GSUB Lookups (initial, isolated, medial, final)
417					//-----------------------------------------------------------------------------------
418					// Arab and Syriac are the only scripts requiring the special joining - which takes the place of
419					// isol fina medi init rules in GSUB (+ fin2 fin3 med2 in Syriac syrc)
420					$tags = 'isol fina fin2 fin3 medi med2 init';
421					$omittags = '';
422					$usetags = $tags;
423					if (!empty($this->mpdf->OTLtags)) {
424						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, true);
425					}
426
427					$this->arabGlyphs = $this->GSUBdata[$this->GSUBfont]['rtlSUB'];
428
429					$gcms = explode("| ", $this->GlyphClassMarks);
430					$gcm = [];
431					foreach ($gcms as $g) {
432						$gcm[hexdec($g)] = 1;
433					}
434					$this->arabTransparentJoin = $this->arabTransparent + $gcm;
435					$this->arabic_shaper($usetags, $GSUBscriptTag);
436
437					//-----------------------------------------------------------------------------------
438					// c. Set Kashida points (after joining occurred - medi, fina, init) but before other substitutions
439					//-----------------------------------------------------------------------------------
440					//if ($scriptblock == Ucdn::SCRIPT_ARABIC ) {
441					for ($i = 0; $i < count($this->OTLdata); $i++) {
442						// Put the kashida marker on the character BEFORE which is inserted the kashida
443						// Kashida marker is inverse of priority i.e. Priority 1 => 7, Priority 7 => 1.
444						// Priority 1   User-inserted Kashida 0640 = Tatweel
445						// The user entered a Kashida in a position
446						// Position: Before the user-inserted kashida
447						if ($this->OTLdata[$i]['uni'] == 0x0640) {
448							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 8; // Put before the next character
449						} // Priority 2   Seen (0633)  FEB3, FEB4; Sad (0635)  FEBB, FEBC
450						// Initial or medial form
451						// Connecting to the next character
452						// Position: After the character
453						elseif ($this->OTLdata[$i]['uni'] == 0xFEB3 || $this->OTLdata[$i]['uni'] == 0xFEB4 || $this->OTLdata[$i]['uni'] == 0xFEBB || $this->OTLdata[$i]['uni'] == 0xFEBC) {
454							$checkpos = $i + 1;
455							while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
456								$checkpos++;
457							}
458							if (isset($this->OTLdata[$checkpos])) {
459								$this->OTLdata[$checkpos]['GPOSinfo']['kashida'] = 7; // Put after marks on next character
460							}
461						} // Priority 3   Taa Marbutah (0629) FE94; Haa (062D) FEA2; Dal (062F) FEAA
462						// Final form
463						// Connecting to previous character
464						// Position: Before the character
465						elseif ($this->OTLdata[$i]['uni'] == 0xFE94 || $this->OTLdata[$i]['uni'] == 0xFEA2 || $this->OTLdata[$i]['uni'] == 0xFEAA) {
466							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 6;
467						} // Priority 4   Alef (0627) FE8E; Tah (0637) FEC2; Lam (0644) FEDE; Kaf (0643)  FEDA; Gaf (06AF) FB93
468						// Final form
469						// Connecting to previous character
470						// Position: Before the character
471						elseif ($this->OTLdata[$i]['uni'] == 0xFE8E || $this->OTLdata[$i]['uni'] == 0xFEC2 || $this->OTLdata[$i]['uni'] == 0xFEDE || $this->OTLdata[$i]['uni'] == 0xFEDA || $this->OTLdata[$i]['uni'] == 0xFB93) {
472							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 5;
473						} // Priority 5   RA (0631) FEAE; Ya (064A)  FEF2 FEF4; Alef Maqsurah (0649) FEF0 FBE9
474						// Final or Medial form
475						// Connected to preceding medial BAA (0628) = FE92
476						// Position: Before preceding medial Baa
477						// Although not mentioned in spec, added Farsi Yeh (06CC) FBFD FBFF; equivalent to 064A or 0649
478						elseif ($this->OTLdata[$i]['uni'] == 0xFEAE || $this->OTLdata[$i]['uni'] == 0xFEF2 || $this->OTLdata[$i]['uni'] == 0xFEF0 || $this->OTLdata[$i]['uni'] == 0xFEF4 || $this->OTLdata[$i]['uni'] == 0xFBE9 || $this->OTLdata[$i]['uni'] == 0xFBFD || $this->OTLdata[$i]['uni'] == 0xFBFF
479						) {
480							$checkpos = $i - 1;
481							while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
482								$checkpos--;
483							}
484							if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == 0xFE92) {
485								$this->OTLdata[$checkpos]['GPOSinfo']['kashida'] = 4; // ******* Before preceding BAA
486							}
487						} // Priority 6   WAW (0648) FEEE; Ain (0639) FECA; Qaf (0642) FED6; Fa (0641) FED2
488						// Final form
489						// Connecting to previous character
490						// Position: Before the character
491						elseif ($this->OTLdata[$i]['uni'] == 0xFEEE || $this->OTLdata[$i]['uni'] == 0xFECA || $this->OTLdata[$i]['uni'] == 0xFED6 || $this->OTLdata[$i]['uni'] == 0xFED2) {
492							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 3;
493						}
494
495						// Priority 7   Other connecting characters
496						// Final form
497						// Connecting to previous character
498						// Position: Before the character
499						/* This isn't in the spec, but using MS WORD as a basis, give a lower priority to the 3 characters already checked
500						  in (5) above. Test case:
501						  &#x62e;&#x652;&#x631;&#x64e;&#x649;&#x670;
502						  &#x641;&#x64e;&#x62a;&#x64f;&#x630;&#x64e;&#x643;&#x651;&#x650;&#x631;
503						 */
504
505						if (!isset($this->OTLdata[$i]['GPOSinfo']['kashida'])) {
506							if (strpos($this->GSUBdata[$this->GSUBfont]['finals'], $this->OTLdata[$i]['hex']) !== false) { // ANY OTHER FINAL FORM
507								$this->OTLdata[$i]['GPOSinfo']['kashida'] = 2;
508							} elseif (strpos('0FEAE 0FEF0 0FEF2', $this->OTLdata[$i]['hex']) !== false) { // not already included in 5 above
509								$this->OTLdata[$i]['GPOSinfo']['kashida'] = 1;
510							}
511						}
512					}
513
514					//-----------------------------------------------------------------------------------
515					// d. Apply Presentation Forms GSUB Lookups (+ any discretionary) - Apply one at a time in Feature order
516					//-----------------------------------------------------------------------------------
517					$tags = 'rlig calt liga clig mset';
518
519					$omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
520					$usetags = $tags;
521					if (!empty($this->mpdf->OTLtags)) {
522						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
523					}
524
525					$ts = explode(' ', $usetags);
526					foreach ($ts as $ut) { //  - Apply one at a time in Feature order
527						$this->_applyGSUBrules($ut, $GSUBscriptTag, $GSUBlangsys);
528					}
529					//-----------------------------------------------------------------------------------
530					// e. NOT IN SPEC
531					// If space precedes a mark -> substitute a &nbsp; before the Mark, to prevent line breaking Test:
532					//-----------------------------------------------------------------------------------
533					for ($ptr = 1; $ptr < count($this->OTLdata); $ptr++) {
534						if ($this->OTLdata[$ptr]['general_category'] == Ucdn::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK && $this->OTLdata[$ptr - 1]['uni'] == 32) {
535							$this->OTLdata[$ptr - 1]['uni'] = 0xa0;
536							$this->OTLdata[$ptr - 1]['hex'] = '000A0';
537						}
538					}
539				} // 5(I). GSUB - Shaper - INDIC and SINHALA and KHMER
540				//===================================
541				elseif ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
542					$this->restrictToSyllable = true;
543					//-----------------------------------------------------------------------------------
544					// a. First decompose/compose split mattras
545					// (normalize) ??????? Nukta/Halant order etc ??????????????????????????????????????????????????????????????????????????
546					//-----------------------------------------------------------------------------------
547					for ($ptr = 0; $ptr < count($this->OTLdata); $ptr++) {
548						$char = $this->OTLdata[$ptr]['uni'];
549						$sub = Indic::decompose_indic($char);
550						if ($sub) {
551							$newinfo = [];
552							for ($i = 0; $i < count($sub); $i++) {
553								$newinfo[$i] = [];
554								$ucd_record = Ucdn::get_ucd_record($sub[$i]);
555								$newinfo[$i]['general_category'] = $ucd_record[0];
556								$newinfo[$i]['bidi_type'] = $ucd_record[2];
557								$charasstr = $this->unicode_hex($sub[$i]);
558								if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
559									$newinfo[$i]['group'] = 'M';
560								} else {
561									$newinfo[$i]['group'] = 'C';
562								}
563								$newinfo[$i]['uni'] = $sub[$i];
564								$newinfo[$i]['hex'] = $charasstr;
565							}
566							array_splice($this->OTLdata, $ptr, 1, $newinfo);
567							$ptr += count($sub) - 1;
568						}
569						/* Only Composition-exclusion exceptions that we want to recompose. */
570						if ($this->shaper == 'I') {
571							if ($char == 0x09AF && isset($this->OTLdata[$ptr + 1]) && $this->OTLdata[$ptr + 1]['uni'] == 0x09BC) {
572								$sub = 0x09DF;
573								$newinfo = [];
574								$newinfo[0] = [];
575								$ucd_record = Ucdn::get_ucd_record($sub);
576								$newinfo[0]['general_category'] = $ucd_record[0];
577								$newinfo[0]['bidi_type'] = $ucd_record[2];
578								$newinfo[0]['group'] = 'C';
579								$newinfo[0]['uni'] = $sub;
580								$newinfo[0]['hex'] = $this->unicode_hex($sub);
581								array_splice($this->OTLdata, $ptr, 2, $newinfo);
582							}
583						}
584					}
585					//-----------------------------------------------------------------------------------
586					// b. Analyse characters - group as syllables/clusters (Indic); invalid diacritics; add dotted circle
587					//-----------------------------------------------------------------------------------
588					$indic_category_string = '';
589					foreach ($this->OTLdata as $eid => $c) {
590						Indic::set_indic_properties($this->OTLdata[$eid], $scriptblock); // sets ['indic_category'] and ['indic_position']
591						//$c['general_category']
592						//$c['combining_class']
593						//$c['uni'] =  $char;
594
595						$indic_category_string .= Indic::$indic_category_char[$this->OTLdata[$eid]['indic_category']];
596					}
597
598					$broken_syllables = false;
599					if ($this->shaper == 'I') {
600						Indic::set_syllables($this->OTLdata, $indic_category_string, $broken_syllables);
601					} elseif ($this->shaper == 'S') {
602						Indic::set_syllables_sinhala($this->OTLdata, $indic_category_string, $broken_syllables);
603					} elseif ($this->shaper == 'K') {
604						Indic::set_syllables_khmer($this->OTLdata, $indic_category_string, $broken_syllables);
605					}
606					$indic_category_string = '';
607
608					//-----------------------------------------------------------------------------------
609					// c. Initial Re-ordering (Indic / Khmer / Sinhala)
610					//-----------------------------------------------------------------------------------
611					// Find base consonant
612					// Decompose/compose and reorder Matras
613					// Reorder marks to canonical order
614
615					$indic_config = Indic::$indic_configs[$scriptblock];
616					$dottedcircle = false;
617					if ($broken_syllables) {
618						if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'], 0x25CC)) {
619							$dottedcircle = [];
620							$ucd_record = Ucdn::get_ucd_record(0x25CC);
621							$dottedcircle[0]['general_category'] = $ucd_record[0];
622							$dottedcircle[0]['bidi_type'] = $ucd_record[2];
623							$dottedcircle[0]['group'] = 'C';
624							$dottedcircle[0]['uni'] = 0x25CC;
625							$dottedcircle[0]['indic_category'] = Indic::OT_DOTTEDCIRCLE;
626							$dottedcircle[0]['indic_position'] = Indic::POS_BASE_C;
627
628							$dottedcircle[0]['hex'] = '025CC';  // TEMPORARY *****
629						}
630					}
631					Indic::initial_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $indic_config, $scriptblock, $is_old_spec, $dottedcircle);
632
633					//-----------------------------------------------------------------------------------
634					// d. Apply initial and basic shaping forms GSUB Lookups (one at a time)
635					//-----------------------------------------------------------------------------------
636					if ($this->shaper == 'I' || $this->shaper == 'S') {
637						$tags = 'locl ccmp nukt akhn rphf rkrf pref blwf half pstf vatu cjct';
638					} elseif ($this->shaper == 'K') {
639						$tags = 'locl ccmp pref blwf abvf pstf cfar';
640					}
641					$this->_applyGSUBrulesIndic($tags, $GSUBscriptTag, $GSUBlangsys, $is_old_spec);
642
643					//-----------------------------------------------------------------------------------
644					// e. Final Re-ordering (Indic / Khmer / Sinhala)
645					//-----------------------------------------------------------------------------------
646					// Reorder matras
647					// Reorder reph
648					// Reorder pre-base reordering consonants:
649
650					Indic::final_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $indic_config, $scriptblock, $is_old_spec);
651
652					//-----------------------------------------------------------------------------------
653					// f. Apply 'init' feature to first syllable in word (indicated by ['mask']) Indic::FLAG(Indic::INIT);
654					//-----------------------------------------------------------------------------------
655					if ($this->shaper == 'I' || $this->shaper == 'S') {
656						$tags = 'init';
657						$this->_applyGSUBrulesIndic($tags, $GSUBscriptTag, $GSUBlangsys, $is_old_spec);
658					}
659
660					//-----------------------------------------------------------------------------------
661					// g. Apply Presentation Forms GSUB Lookups (+ any discretionary)
662					//-----------------------------------------------------------------------------------
663					$tags = 'pres abvs blws psts haln rlig calt liga clig mset';
664
665					$omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
666					$usetags = $tags;
667					if (!empty($this->mpdf->OTLtags)) {
668						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
669					}
670					if ($this->shaper == 'K') {  // Features are applied one at a time, working through each codepoint
671						$this->_applyGSUBrulesSingly($usetags, $GSUBscriptTag, $GSUBlangsys);
672					} else {
673						$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
674					}
675					$this->restrictToSyllable = false;
676				} // 5(M). GSUB - Shaper - MYANMAR (ONLY mym2)
677				//==============================
678				// NB Old style 'mymr' is left to go through the default shaper
679				elseif ($this->shaper == 'M') {
680					$this->restrictToSyllable = true;
681					//-----------------------------------------------------------------------------------
682					// a. Analyse characters - group as syllables/clusters (Myanmar); invalid diacritics; add dotted circle
683					//-----------------------------------------------------------------------------------
684					$myanmar_category_string = '';
685					foreach ($this->OTLdata as $eid => $c) {
686						Myanmar::set_myanmar_properties($this->OTLdata[$eid]); // sets ['myanmar_category'] and ['myanmar_position']
687						$myanmar_category_string .= Myanmar::$myanmar_category_char[$this->OTLdata[$eid]['myanmar_category']];
688					}
689					$broken_syllables = false;
690					Myanmar::set_syllables($this->OTLdata, $myanmar_category_string, $broken_syllables);
691					$myanmar_category_string = '';
692
693					//-----------------------------------------------------------------------------------
694					// b. Re-ordering (Myanmar mym2)
695					//-----------------------------------------------------------------------------------
696					$dottedcircle = false;
697					if ($broken_syllables) {
698						if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'], 0x25CC)) {
699							$dottedcircle = [];
700							$ucd_record = Ucdn::get_ucd_record(0x25CC);
701							$dottedcircle[0]['general_category'] = $ucd_record[0];
702							$dottedcircle[0]['bidi_type'] = $ucd_record[2];
703							$dottedcircle[0]['group'] = 'C';
704							$dottedcircle[0]['uni'] = 0x25CC;
705							$dottedcircle[0]['myanmar_category'] = Myanmar::OT_DOTTEDCIRCLE;
706							$dottedcircle[0]['myanmar_position'] = Myanmar::POS_BASE_C;
707							$dottedcircle[0]['hex'] = '025CC';
708						}
709					}
710					Myanmar::reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $dottedcircle);
711
712					//-----------------------------------------------------------------------------------
713					// c. Apply initial and basic shaping forms GSUB Lookups (one at a time)
714					//-----------------------------------------------------------------------------------
715
716					$tags = 'locl ccmp rphf pref blwf pstf';
717					$this->_applyGSUBrulesMyanmar($tags, $GSUBscriptTag, $GSUBlangsys);
718
719					//-----------------------------------------------------------------------------------
720					// d. Apply Presentation Forms GSUB Lookups (+ any discretionary)
721					//-----------------------------------------------------------------------------------
722					$tags = 'pres abvs blws psts haln rlig calt liga clig mset';
723					$omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
724					$usetags = $tags;
725					if (!empty($this->mpdf->OTLtags)) {
726						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
727					}
728					$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
729					$this->restrictToSyllable = false;
730				} // 5(E). GSUB - Shaper - SEA South East Asian (New Tai Lue, Cham, Tai Tam)
731				//==============================
732				elseif ($this->shaper == 'E') {
733					/* HarfBuzz says: If the designer designed the font for the 'DFLT' script,
734					 * use the default shaper.  Otherwise, use the SEA shaper.
735					 * Note that for some simple scripts, there may not be *any*
736					 * GSUB/GPOS needed, so there may be no scripts found! */
737
738					$this->restrictToSyllable = true;
739					//-----------------------------------------------------------------------------------
740					// a. Analyse characters - group as syllables/clusters (Indic); invalid diacritics; add dotted circle
741					//-----------------------------------------------------------------------------------
742					$sea_category_string = '';
743					foreach ($this->OTLdata as $eid => $c) {
744						Sea::set_sea_properties($this->OTLdata[$eid], $scriptblock); // sets ['sea_category'] and ['sea_position']
745						//$c['general_category']
746						//$c['combining_class']
747						//$c['uni'] =  $char;
748
749						$sea_category_string .= Sea::$sea_category_char[$this->OTLdata[$eid]['sea_category']];
750					}
751
752					$broken_syllables = false;
753					Sea::set_syllables($this->OTLdata, $sea_category_string, $broken_syllables);
754					$sea_category_string = '';
755
756					//-----------------------------------------------------------------------------------
757					// b. Apply locl and ccmp shaping forms - before initial re-ordering; GSUB Lookups (one at a time)
758					//-----------------------------------------------------------------------------------
759					$tags = 'locl ccmp';
760					$this->_applyGSUBrulesSingly($tags, $GSUBscriptTag, $GSUBlangsys);
761
762					//-----------------------------------------------------------------------------------
763					// c. Initial Re-ordering
764					//-----------------------------------------------------------------------------------
765					// Find base consonant
766					// Decompose/compose and reorder Matras
767					// Reorder marks to canonical order
768
769					$dottedcircle = false;
770					if ($broken_syllables) {
771						if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'], 0x25CC)) {
772							$dottedcircle = [];
773							$ucd_record = Ucdn::get_ucd_record(0x25CC);
774							$dottedcircle[0]['general_category'] = $ucd_record[0];
775							$dottedcircle[0]['bidi_type'] = $ucd_record[2];
776							$dottedcircle[0]['group'] = 'C';
777							$dottedcircle[0]['uni'] = 0x25CC;
778							$dottedcircle[0]['sea_category'] = Sea::OT_GB;
779							$dottedcircle[0]['sea_position'] = Sea::POS_BASE_C;
780
781							$dottedcircle[0]['hex'] = '025CC';  // TEMPORARY *****
782						}
783					}
784					Sea::initial_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $scriptblock, $dottedcircle);
785
786					//-----------------------------------------------------------------------------------
787					// d. Apply basic shaping forms GSUB Lookups (one at a time)
788					//-----------------------------------------------------------------------------------
789					$tags = 'pref abvf blwf pstf';
790					$this->_applyGSUBrulesSingly($tags, $GSUBscriptTag, $GSUBlangsys);
791
792					//-----------------------------------------------------------------------------------
793					// e. Final Re-ordering
794					//-----------------------------------------------------------------------------------
795
796					Sea::final_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $scriptblock);
797
798					//-----------------------------------------------------------------------------------
799					// f. Apply Presentation Forms GSUB Lookups (+ any discretionary)
800					//-----------------------------------------------------------------------------------
801					$tags = 'pres abvs blws psts';
802
803					$omittags = 'locl ccmp nukt akhn rphf rkrf pref blwf abvf half pstf cfar vatu cjct init medi fina isol med2 fin2 fin3 ljmo vjmo tjmo';
804					$usetags = $tags;
805					if (!empty($this->mpdf->OTLtags)) {
806						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
807					}
808					$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
809					$this->restrictToSyllable = false;
810				} // 5(D). GSUB - Shaper - DEFAULT (including THAI and LAO and MYANMAR v1 [mymr] and TIBETAN)
811				//==============================
812				else { // DEFAULT
813					//-----------------------------------------------------------------------------------
814					// a. First decompose/compose in Thai / Lao - Tibetan
815					//-----------------------------------------------------------------------------------
816					// Decomposition for THAI or LAO
817					/* This function implements the shaping logic documented here:
818					 *
819					 *   http://linux.thai.net/~thep/th-otf/shaping.html
820					 *
821					 * The first shaping rule listed there is needed even if the font has Thai
822					 * OpenType tables.
823					 *
824					 *
825					 * The following is NOT specified in the MS OT Thai spec, however, it seems
826					 * to be what Uniscribe and other engines implement.  According to Eric Muller:
827					 *
828					 * When you have a SARA AM, decompose it in NIKHAHIT + SARA AA, *and* move the
829					 * NIKHAHIT backwards over any tone mark (0E48-0E4B).
830					 *
831					 * <0E14, 0E4B, 0E33> -> <0E14, 0E4D, 0E4B, 0E32>
832					 *
833					 * This reordering is legit only when the NIKHAHIT comes from a SARA AM, not
834					 * when it's there to start with. The string <0E14, 0E4B, 0E4D> is probably
835					 * not what a user wanted, but the rendering is nevertheless nikhahit above
836					 * chattawa.
837					 *
838					 * Same for Lao.
839					 *
840					 *          Thai        Lao
841					 * SARA AM:     U+0E33  U+0EB3
842					 * SARA AA:     U+0E32  U+0EB2
843					 * Nikhahit:    U+0E4D  U+0ECD
844					 *
845					 * Testing shows that Uniscribe reorder the following marks:
846					 * Thai:    <0E31,0E34..0E37,0E47..0E4E>
847					 * Lao: <0EB1,0EB4..0EB7,0EC7..0ECE>
848					 *
849					 * Lao versions are the same as Thai + 0x80.
850					 */
851					if ($this->shaper == 'T' || $this->shaper == 'L') {
852						for ($ptr = 0; $ptr < count($this->OTLdata); $ptr++) {
853							$char = $this->OTLdata[$ptr]['uni'];
854							if (($char & ~0x0080) == 0x0E33) { // if SARA_AM (U+0E33 or U+0EB3)
855								$NIKHAHIT = $char + 0x1A;
856								$SARA_AA = $char - 1;
857								$sub = [$SARA_AA, $NIKHAHIT];
858
859								$newinfo = [];
860								$ucd_record = Ucdn::get_ucd_record($sub[0]);
861								$newinfo[0]['general_category'] = $ucd_record[0];
862								$newinfo[0]['bidi_type'] = $ucd_record[2];
863								$charasstr = $this->unicode_hex($sub[0]);
864								if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
865									$newinfo[0]['group'] = 'M';
866								} else {
867									$newinfo[0]['group'] = 'C';
868								}
869								$newinfo[0]['uni'] = $sub[0];
870								$newinfo[0]['hex'] = $charasstr;
871								$this->OTLdata[$ptr] = $newinfo[0]; // Substitute SARA_AM => SARA_AA
872
873								$ntones = 0; // number of (preceding) tone marks
874								// IS_TONE_MARK ((x) & ~0x0080, 0x0E34 - 0x0E37, 0x0E47 - 0x0E4E, 0x0E31)
875								while (isset($this->OTLdata[$ptr - 1 - $ntones]) && (
876								($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) == 0x0E31 ||
877								(($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) >= 0x0E34 &&
878								($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) <= 0x0E37) ||
879								(($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) >= 0x0E47 &&
880								($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) <= 0x0E4E)
881								)
882								) {
883									$ntones++;
884								}
885
886								$newinfo = [];
887								$ucd_record = Ucdn::get_ucd_record($sub[1]);
888								$newinfo[0]['general_category'] = $ucd_record[0];
889								$newinfo[0]['bidi_type'] = $ucd_record[2];
890								$charasstr = $this->unicode_hex($sub[1]);
891								if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
892									$newinfo[0]['group'] = 'M';
893								} else {
894									$newinfo[0]['group'] = 'C';
895								}
896								$newinfo[0]['uni'] = $sub[1];
897								$newinfo[0]['hex'] = $charasstr;
898								// Insert NIKAHIT
899								array_splice($this->OTLdata, $ptr - $ntones, 0, $newinfo);
900
901								$ptr++;
902							}
903						}
904					}
905
906					if ($scriptblock == Ucdn::SCRIPT_TIBETAN) {
907						// =========================
908						// Reordering TIBETAN
909						// =========================
910						// Tibetan does not need to need a shaper generally, as long as characters are presented in the correct order
911						// so we will do one minor change here:
912						// From ICU: If the present character is a number, and the next character is a pre-number combining mark
913						// then the two characters are reordered
914						// From MS OTL spec the following are Digit modifiers (Md): 0F18–0F19, 0F3E–0F3F
915						// Digits: 0F20–0F33
916						// On testing only 0x0F3F (pre-based mark) seems to need re-ordering
917						for ($ptr = 0; $ptr < count($this->OTLdata) - 1; $ptr++) {
918							if (Indic::in_range($this->OTLdata[$ptr]['uni'], 0x0F20, 0x0F33) && $this->OTLdata[$ptr + 1]['uni'] == 0x0F3F) {
919								$tmp = $this->OTLdata[$ptr + 1];
920								$this->OTLdata[$ptr + 1] = $this->OTLdata[$ptr];
921								$this->OTLdata[$ptr] = $tmp;
922							}
923						}
924
925
926						// =========================
927						// Decomposition for TIBETAN
928						// =========================
929						/* Recommended, but does not seem to change anything...
930						  for($ptr=0; $ptr<count($this->OTLdata); $ptr++) {
931						  $char = $this->OTLdata[$ptr]['uni'];
932						  $sub = Indic::decompose_indic($char);
933						  if ($sub) {
934						  $newinfo = array();
935						  for($i=0;$i<count($sub);$i++) {
936						  $newinfo[$i] = array();
937						  $ucd_record = Ucdn::get_ucd_record($sub[$i]);
938						  $newinfo[$i]['general_category'] = $ucd_record[0];
939						  $newinfo[$i]['bidi_type'] = $ucd_record[2];
940						  $charasstr = $this->unicode_hex($sub[$i]);
941						  if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $newinfo[$i]['group'] =  'M'; }
942						  else { $newinfo[$i]['group'] =  'C'; }
943						  $newinfo[$i]['uni'] =  $sub[$i];
944						  $newinfo[$i]['hex'] =  $charasstr;
945						  }
946						  array_splice($this->OTLdata, $ptr, 1, $newinfo);
947						  $ptr += count($sub)-1;
948						  }
949						  }
950						 */
951					}
952
953
954					//-----------------------------------------------------------------------------------
955					// b. Apply all GSUB Lookups (in order specified in lookup list)
956					//-----------------------------------------------------------------------------------
957					$tags = 'locl ccmp pref blwf abvf pstf pres abvs blws psts haln rlig calt liga clig mset  RQD';
958					// pref blwf abvf pstf required for Tibetan
959					// " RQD" is a non-standard tag in Garuda font - presumably intended to be used by default ? "ReQuireD"
960					// Being a 3 letter tag is non-standard, and does not allow it to be set by font-feature-settings
961
962
963					/* ?Add these until shapers witten?
964					  Hangul:   ljmo vjmo tjmo
965					 */
966
967					$omittags = '';
968					$useGSUBtags = $tags;
969					if (!empty($this->mpdf->OTLtags)) {
970						$useGSUBtags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
971					}
972					// APPLY GSUB rules (as long as not Latin + SmallCaps - but not OTL smcp)
973					if (!(($this->mpdf->textvar & TextVars::FC_SMALLCAPS) && $scriptblock == Ucdn::SCRIPT_LATIN && strpos($useGSUBtags, 'smcp') === false)) {
974						$this->_applyGSUBrules($useGSUBtags, $GSUBscriptTag, $GSUBlangsys);
975					}
976				}
977			}
978
979			// Shapers - KHMER & THAI & LAO - Replace Word boundary marker with U+200B
980			// Also TIBETAN (no shaper)
981			//=======================================================
982			if (($this->shaper == "K" || $this->shaper == "T" || $this->shaper == "L") || $scriptblock == Ucdn::SCRIPT_TIBETAN) {
983				// Set up properties to insert a U+200B character
984				$newinfo = [];
985				//$newinfo[0] = array('general_category' => 1, 'bidi_type' => 14, 'group' => 'S', 'uni' => 0x200B, 'hex' => '0200B');
986				$newinfo[0] = [
987					'general_category' => Ucdn::UNICODE_GENERAL_CATEGORY_FORMAT,
988					'bidi_type' => Ucdn::BIDI_CLASS_BN,
989					'group' => 'S', 'uni' => 0x200B, 'hex' => '0200B'];
990				// Then insert U+200B at (after) all word end boundaries
991				for ($i = count($this->OTLdata) - 1; $i > 0; $i--) {
992					// Make sure after GSUB that wordend has not been moved - check next char is not in the same syllable
993					if (isset($this->OTLdata[$i]['wordend']) && $this->OTLdata[$i]['wordend'] &&
994						isset($this->OTLdata[$i + 1]['uni']) && (!isset($this->OTLdata[$i + 1]['syllable']) || !isset($this->OTLdata[$i + 1]['syllable']) || $this->OTLdata[$i + 1]['syllable'] != $this->OTLdata[$i]['syllable'])) {
995						array_splice($this->OTLdata, $i + 1, 0, $newinfo);
996						$this->_updateLigatureMarks($i, 1);
997					} elseif ($this->OTLdata[$i]['uni'] == 0x2e) { // Word end if Full-stop.
998						array_splice($this->OTLdata, $i + 1, 0, $newinfo);
999						$this->_updateLigatureMarks($i, 1);
1000					}
1001				}
1002			}
1003
1004
1005			// Shapers - INDIC & ARABIC & KHMER & SINHALA  & MYANMAR - Remove ZWJ and ZWNJ
1006			//=======================================================
1007			if ($this->shaper == 'I' || $this->shaper == 'S' || $this->shaper == 'A' || $this->shaper == 'K' || $this->shaper == 'M') {
1008				// Remove ZWJ and ZWNJ
1009				for ($i = 0; $i < count($this->OTLdata); $i++) {
1010					if ($this->OTLdata[$i]['uni'] == 8204 || $this->OTLdata[$i]['uni'] == 8205) {
1011						array_splice($this->OTLdata, $i, 1);
1012						$this->_updateLigatureMarks($i, -1);
1013					}
1014				}
1015			}
1016
1017
1018			////////////////////////////////////////////////////////////////
1019			////////////////////////////////////////////////////////////////
1020			//////////       GPOS          /////////////////////////////////
1021			////////////////////////////////////////////////////////////////
1022			////////////////////////////////////////////////////////////////
1023			if (($useOTL & 0xFF) && $GPOSscriptTag && $GPOSlangsys && $GPOSFeatures) {
1024				$this->Entry = [];
1025				$this->Exit = [];
1026
1027				// 6. Load GPOS data, Coverage & Lookups
1028				//=================================================================
1029				$fontCacheFilename = $this->mpdf->CurrentFont['fontkey'] . '.GPOSdata.json';
1030				if (!isset($this->GPOSdata[$this->fontkey]) && $this->fontCache->jsonHas($fontCacheFilename)) {
1031					$this->LuCoverage = $this->GPOSdata[$this->fontkey]['LuCoverage'] = $this->fontCache->jsonLoad($fontCacheFilename);
1032				} else {
1033					$this->LuCoverage = $this->GPOSdata[$this->fontkey]['LuCoverage'];
1034				}
1035
1036				$this->GPOSLookups = $this->mpdf->CurrentFont['GPOSLookups'];
1037
1038
1039				// 7. Select Feature tags to use (incl optional)
1040				//==============================
1041				$tags = 'abvm blwm mark mkmk curs cpsp dist requ'; // Default set
1042				// 'requ' is not listed in the Microsoft registry of Feature tags
1043				// Found in Arial Unicode MS, it repositions the baseline for punctuation in Kannada script
1044
1045				// ZZZ96
1046				// Set kern to be included by default in non-Latin script (? just when shapers used)
1047				// Kern is used in some fonts to reposition marks etc. and is essential for correct display
1048				//if ($this->shaper) {$tags .= ' kern'; }
1049				if ($scriptblock != Ucdn::SCRIPT_LATIN) {
1050					$tags .= ' kern';
1051				}
1052
1053				$omittags = '';
1054				$usetags = $tags;
1055				if (!empty($this->mpdf->OTLtags)) {
1056					$usetags = $this->_applyTagSettings($tags, $GPOSFeatures, $omittags, false);
1057				}
1058
1059
1060
1061				// 8. Get GPOS LookupList from Feature tags
1062				//==============================
1063				$LookupList = [];
1064				foreach ($GPOSFeatures as $tag => $arr) {
1065					if (strpos($usetags, $tag) !== false) {
1066						foreach ($arr as $lu) {
1067							$LookupList[$lu] = $tag;
1068						}
1069					}
1070				}
1071				ksort($LookupList);
1072
1073
1074				// 9. Apply GPOS Lookups (in order specified in lookup list but selecting from specified tags)
1075				//==============================
1076				// APPLY THE GPOS RULES (as long as not Latin + SmallCaps - but not OTL smcp)
1077				if (!(($this->mpdf->textvar & TextVars::FC_SMALLCAPS) && $scriptblock == Ucdn::SCRIPT_LATIN && strpos($useGSUBtags, 'smcp') === false)) {
1078					$this->_applyGPOSrules($LookupList, $is_old_spec);
1079					// (sets: $this->OTLdata[n]['GPOSinfo'] XPlacement YPlacement XAdvance Entry Exit )
1080				}
1081
1082				// 10. Process cursive text
1083				//==============================
1084				if (count($this->Entry) || count($this->Exit)) {
1085					// RTL
1086					$incurs = false;
1087					for ($i = (count($this->OTLdata) - 1); $i >= 0; $i--) {
1088						if (isset($this->Entry[$i]) && isset($this->Entry[$i]['Y']) && $this->Entry[$i]['dir'] == 'RTL') {
1089							$nextbase = $i - 1; // Set as next base ignoring marks (next base reading RTL in logical oder
1090							while (isset($this->OTLdata[$nextbase]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex']) !== false) {
1091								$nextbase--;
1092							}
1093							if (isset($this->Exit[$nextbase]) && isset($this->Exit[$nextbase]['Y'])) {
1094								$diff = $this->Entry[$i]['Y'] - $this->Exit[$nextbase]['Y'];
1095								if ($incurs === false) {
1096									$incurs = $diff;
1097								} else {
1098									$incurs += $diff;
1099								}
1100								for ($j = ($i - 1); $j >= $nextbase; $j--) {
1101									if (isset($this->OTLdata[$j]['GPOSinfo']['YPlacement'])) {
1102										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] += $incurs;
1103									} else {
1104										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] = $incurs;
1105									}
1106								}
1107								if (isset($this->Exit[$i]['X']) && isset($this->Entry[$nextbase]['X'])) {
1108									$adj = -($this->Entry[$i]['X'] - $this->Exit[$nextbase]['X']);
1109									// If XAdvance is aplied - in order for PDF to position the Advance correctly need to place it on:
1110									// in RTL - the current glyph or the last of any associated marks
1111									if (isset($this->OTLdata[$nextbase + 1]['GPOSinfo']['XAdvance'])) {
1112										$this->OTLdata[$nextbase + 1]['GPOSinfo']['XAdvance'] += $adj;
1113									} else {
1114										$this->OTLdata[$nextbase + 1]['GPOSinfo']['XAdvance'] = $adj;
1115									}
1116								}
1117							} else {
1118								$incurs = false;
1119							}
1120						} elseif (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex']) !== false) {
1121							continue;
1122						} // ignore Marks
1123						else {
1124							$incurs = false;
1125						}
1126					}
1127					// LTR
1128					$incurs = false;
1129					for ($i = 0; $i < count($this->OTLdata); $i++) {
1130						if (isset($this->Exit[$i]) && isset($this->Exit[$i]['Y']) && $this->Exit[$i]['dir'] == 'LTR') {
1131							$nextbase = $i + 1; // Set as next base ignoring marks
1132							while (strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex']) !== false) {
1133								$nextbase++;
1134							}
1135							if (isset($this->Entry[$nextbase]) && isset($this->Entry[$nextbase]['Y'])) {
1136								$diff = $this->Exit[$i]['Y'] - $this->Entry[$nextbase]['Y'];
1137								if ($incurs === false) {
1138									$incurs = $diff;
1139								} else {
1140									$incurs += $diff;
1141								}
1142								for ($j = ($i + 1); $j <= $nextbase; $j++) {
1143									if (isset($this->OTLdata[$j]['GPOSinfo']['YPlacement'])) {
1144										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] += $incurs;
1145									} else {
1146										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] = $incurs;
1147									}
1148								}
1149								if (isset($this->Exit[$i]['X']) && isset($this->Entry[$nextbase]['X'])) {
1150									$adj = -($this->Exit[$i]['X'] - $this->Entry[$nextbase]['X']);
1151									// If XAdvance is aplied - in order for PDF to position the Advance correctly need to place it on:
1152									// in LTR - the next glyph, ignoring marks
1153									if (isset($this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'])) {
1154										$this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'] += $adj;
1155									} else {
1156										$this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'] = $adj;
1157									}
1158								}
1159							} else {
1160								$incurs = false;
1161							}
1162						} elseif (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex']) !== false) {
1163							continue;
1164						} // ignore Marks
1165						else {
1166							$incurs = false;
1167						}
1168					}
1169				}
1170			} // end GPOS
1171
1172			if ($this->debugOTL) {
1173				$this->_dumpproc('END', '-', '-', '-', '-', 0, '-', 0);
1174				exit;
1175			}
1176
1177			$this->schOTLdata[$sch] = $this->OTLdata;
1178			$this->OTLdata = [];
1179		} // END foreach subchunk
1180		// 11. Re-assemble and return text string
1181		//==============================
1182		$newGPOSinfo = [];
1183		$newOTLdata = [];
1184		$newchar_data = [];
1185		$newgroup = '';
1186		$e = '';
1187		$ectr = 0;
1188
1189		for ($sch = 0; $sch <= $subchunk; $sch++) {
1190			for ($i = 0; $i < count($this->schOTLdata[$sch]); $i++) {
1191				if (isset($this->schOTLdata[$sch][$i]['GPOSinfo'])) {
1192					$newGPOSinfo[$ectr] = $this->schOTLdata[$sch][$i]['GPOSinfo'];
1193				}
1194				$newchar_data[$ectr] = ['bidi_class' => $this->schOTLdata[$sch][$i]['bidi_type'], 'uni' => $this->schOTLdata[$sch][$i]['uni']];
1195				$newgroup .= $this->schOTLdata[$sch][$i]['group'];
1196				$e .= UtfString::code2utf($this->schOTLdata[$sch][$i]['uni']);
1197				if (isset($this->mpdf->CurrentFont['subset'])) {
1198					$this->mpdf->CurrentFont['subset'][$this->schOTLdata[$sch][$i]['uni']] = $this->schOTLdata[$sch][$i]['uni'];
1199				}
1200				$ectr++;
1201			}
1202		}
1203		$this->OTLdata['GPOSinfo'] = $newGPOSinfo;
1204		$this->OTLdata['char_data'] = $newchar_data;
1205		$this->OTLdata['group'] = $newgroup;
1206
1207		// This leaves OTLdata::GPOSinfo, ::bidi_type, & ::group
1208
1209		return $e;
1210	}
1211
1212	function _applyTagSettings($tags, $Features, $omittags = '', $onlytags = false)
1213	{
1214		if (empty($this->mpdf->OTLtags['Plus']) && empty($this->mpdf->OTLtags['Minus']) && empty($this->mpdf->OTLtags['FFPlus']) && empty($this->mpdf->OTLtags['FFMinus'])) {
1215			return $tags;
1216		}
1217
1218		// Use $tags as starting point
1219		$usetags = $tags;
1220
1221		// Only set / unset tags which are in the font
1222		// Ignore tags which are in $omittags
1223		// If $onlytags, then just unset tags which are already in the Tag list
1224
1225		$fp = $fm = $ffp = $ffm = '';
1226
1227		// Font features to enable - set by font-variant-xx
1228		if (isset($this->mpdf->OTLtags['Plus'])) {
1229			$fp = $this->mpdf->OTLtags['Plus'];
1230		}
1231		preg_match_all('/([a-zA-Z0-9]{4})/', $fp, $m);
1232		for ($i = 0; $i < count($m[0]); $i++) {
1233			$t = $m[1][$i];
1234			// Is it a valid tag?
1235			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1236				$usetags .= ' ' . $t;
1237			}
1238		}
1239
1240		// Font features to disable - set by font-variant-xx
1241		if (isset($this->mpdf->OTLtags['Minus'])) {
1242			$fm = $this->mpdf->OTLtags['Minus'];
1243		}
1244		preg_match_all('/([a-zA-Z0-9]{4})/', $fm, $m);
1245		for ($i = 0; $i < count($m[0]); $i++) {
1246			$t = $m[1][$i];
1247			// Is it a valid tag?
1248			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1249				$usetags = str_replace($t, '', $usetags);
1250			}
1251		}
1252
1253		// Font features to enable - set by font-feature-settings
1254		if (isset($this->mpdf->OTLtags['FFPlus'])) {
1255			$ffp = $this->mpdf->OTLtags['FFPlus']; // Font Features - may include integer: salt4
1256		}
1257		preg_match_all('/([a-zA-Z0-9]{4})([\d+]*)/', $ffp, $m);
1258		for ($i = 0; $i < count($m[0]); $i++) {
1259			$t = $m[1][$i];
1260			// Is it a valid tag?
1261			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1262				$usetags .= ' ' . $m[0][$i];  //  - may include integer: salt4
1263			}
1264		}
1265
1266		// Font features to disable - set by font-feature-settings
1267		if (isset($this->mpdf->OTLtags['FFMinus'])) {
1268			$ffm = $this->mpdf->OTLtags['FFMinus'];
1269		}
1270		preg_match_all('/([a-zA-Z0-9]{4})/', $ffm, $m);
1271		for ($i = 0; $i < count($m[0]); $i++) {
1272			$t = $m[1][$i];
1273			// Is it a valid tag?
1274			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1275				$usetags = str_replace($t, '', $usetags);
1276			}
1277		}
1278		return $usetags;
1279	}
1280
1281	function _applyGSUBrules($usetags, $scriptTag, $langsys)
1282	{
1283		// Features from all Tags are applied together, in Lookup List order.
1284		// For Indic - should be applied one syllable at a time
1285		// - Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1286		// if $this->restrictToSyllable is true
1287
1288		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1289		$LookupList = [];
1290		foreach ($GSUBFeatures as $tag => $arr) {
1291			if (strpos($usetags, $tag) !== false) {
1292				foreach ($arr as $lu) {
1293					$LookupList[$lu] = $tag;
1294				}
1295			}
1296		}
1297		ksort($LookupList);
1298
1299		foreach ($LookupList as $lu => $tag) {
1300			$Type = $this->GSUBLookups[$lu]['Type'];
1301			$Flag = $this->GSUBLookups[$lu]['Flag'];
1302			$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1303			$tagInt = 1;
1304			if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1305				$tagInt = $m[1];
1306			}
1307			$ptr = 0;
1308			// Test each glyph sequentially
1309			while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1310				$currGlyph = $this->OTLdata[$ptr]['hex'];
1311				$currGID = $this->OTLdata[$ptr]['uni'];
1312				$shift = 1;
1313				foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1314					// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1315					if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1316						// Get rules from font GSUB subtable
1317						$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $tag, 0, $tagInt);
1318
1319						if ($shift) {
1320							break;
1321						}
1322					}
1323				}
1324				if ($shift == 0) {
1325					$shift = 1;
1326				}
1327				$ptr += $shift;
1328			}
1329		}
1330	}
1331
1332	function _applyGSUBrulesSingly($usetags, $scriptTag, $langsys)
1333	{
1334		// Features are applied one at a time, working through each codepoint
1335
1336		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1337
1338		$tags = explode(' ', $usetags);
1339		foreach ($tags as $usetag) {
1340			$LookupList = [];
1341			foreach ($GSUBFeatures as $tag => $arr) {
1342				if (strpos($usetags, $tag) !== false) {
1343					foreach ($arr as $lu) {
1344						$LookupList[$lu] = $tag;
1345					}
1346				}
1347			}
1348			ksort($LookupList);
1349
1350			$ptr = 0;
1351			// Test each glyph sequentially
1352			while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1353				$currGlyph = $this->OTLdata[$ptr]['hex'];
1354				$currGID = $this->OTLdata[$ptr]['uni'];
1355				$shift = 1;
1356
1357				foreach ($LookupList as $lu => $tag) {
1358					$Type = $this->GSUBLookups[$lu]['Type'];
1359					$Flag = $this->GSUBLookups[$lu]['Flag'];
1360					$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1361					$tagInt = 1;
1362					if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1363						$tagInt = $m[1];
1364					}
1365
1366					foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1367						// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1368						if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1369							// Get rules from font GSUB subtable
1370							$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $tag, 0, $tagInt);
1371
1372							if ($shift) {
1373								break 2;
1374							}
1375						}
1376					}
1377				}
1378				if ($shift == 0) {
1379					$shift = 1;
1380				}
1381				$ptr += $shift;
1382			}
1383		}
1384	}
1385
1386	function _applyGSUBrulesMyanmar($usetags, $scriptTag, $langsys)
1387	{
1388		// $usetags = locl ccmp rphf pref blwf pstf';
1389		// applied to all characters
1390
1391		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1392
1393		// ALL should be applied one syllable at a time
1394		// Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1395		$tags = explode(' ', $usetags);
1396		foreach ($tags as $usetag) {
1397			$LookupList = [];
1398			foreach ($GSUBFeatures as $tag => $arr) {
1399				if ($tag == $usetag) {
1400					foreach ($arr as $lu) {
1401						$LookupList[$lu] = $tag;
1402					}
1403				}
1404			}
1405			ksort($LookupList);
1406
1407			foreach ($LookupList as $lu => $tag) {
1408				$Type = $this->GSUBLookups[$lu]['Type'];
1409				$Flag = $this->GSUBLookups[$lu]['Flag'];
1410				$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1411				$tagInt = 1;
1412				if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1413					$tagInt = $m[1];
1414				}
1415
1416				$ptr = 0;
1417				// Test each glyph sequentially
1418				while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1419					$currGlyph = $this->OTLdata[$ptr]['hex'];
1420					$currGID = $this->OTLdata[$ptr]['uni'];
1421					$shift = 1;
1422					foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1423						// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1424						if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1425							// Get rules from font GSUB subtable
1426							$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $usetag, 0, $tagInt);
1427
1428							if ($shift) {
1429								break;
1430							}
1431						}
1432					}
1433					if ($shift == 0) {
1434						$shift = 1;
1435					}
1436					$ptr += $shift;
1437				}
1438			}
1439		}
1440	}
1441
1442	function _applyGSUBrulesIndic($usetags, $scriptTag, $langsys, $is_old_spec)
1443	{
1444		// $usetags = 'locl ccmp nukt akhn rphf rkrf pref blwf half pstf vatu cjct'; then later - init
1445		// rphf, pref, blwf, half, abvf, pstf, and init are only applied where ['mask'] indicates:  Indic::FLAG(Indic::RPHF);
1446		// The rest are applied to all characters
1447
1448		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1449
1450		// ALL should be applied one syllable at a time
1451		// Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1452		$tags = explode(' ', $usetags);
1453		foreach ($tags as $usetag) {
1454			$LookupList = [];
1455			foreach ($GSUBFeatures as $tag => $arr) {
1456				if ($tag == $usetag) {
1457					foreach ($arr as $lu) {
1458						$LookupList[$lu] = $tag;
1459					}
1460				}
1461			}
1462			ksort($LookupList);
1463
1464			foreach ($LookupList as $lu => $tag) {
1465				$Type = $this->GSUBLookups[$lu]['Type'];
1466				$Flag = $this->GSUBLookups[$lu]['Flag'];
1467				$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1468				$tagInt = 1;
1469				if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1470					$tagInt = $m[1];
1471				}
1472
1473				$ptr = 0;
1474				// Test each glyph sequentially
1475				while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1476					$currGlyph = $this->OTLdata[$ptr]['hex'];
1477					$currGID = $this->OTLdata[$ptr]['uni'];
1478					$shift = 1;
1479					foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1480						// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1481						if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1482							if (strpos('rphf pref blwf half pstf cfar init', $usetag) !== false) { // only apply when mask indicates
1483								$mask = 0;
1484								switch ($usetag) {
1485									case 'rphf':
1486										$mask = (1 << (Indic::RPHF));
1487										break;
1488									case 'pref':
1489										$mask = (1 << (Indic::PREF));
1490										break;
1491									case 'blwf':
1492										$mask = (1 << (Indic::BLWF));
1493										break;
1494									case 'half':
1495										$mask = (1 << (Indic::HALF));
1496										break;
1497									case 'pstf':
1498										$mask = (1 << (Indic::PSTF));
1499										break;
1500									case 'cfar':
1501										$mask = (1 << (Indic::CFAR));
1502										break;
1503									case 'init':
1504										$mask = (1 << (Indic::INIT));
1505										break;
1506								}
1507								if (!($this->OTLdata[$ptr]['mask'] & $mask)) {
1508									continue;
1509								}
1510							}
1511							// Get rules from font GSUB subtable
1512							$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $usetag, $is_old_spec, $tagInt);
1513
1514							if ($shift) {
1515								break;
1516							}
1517						} // Special case for Indic  ZZZ99S
1518						// Check to substitute Halant-Consonant in PREF, BLWF or PSTF
1519						// i.e. new spec but GSUB tables have Consonant-Halant in Lookups e.g. FreeSerif, which
1520						// incorrectly just moved old spec tables to new spec. Uniscribe seems to cope with this
1521						// See also ttffontsuni.php
1522						// First check if current glyph is a Halant/Virama
1523						elseif (static::_OTL_OLD_SPEC_COMPAT_1 && $Type == 4 && !$is_old_spec && strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', $currGlyph) !== false) {
1524							// only apply when 'pref blwf pstf' tags, and when mask indicates
1525							if (strpos('pref blwf pstf', $usetag) !== false) {
1526								$mask = 0;
1527								switch ($usetag) {
1528									case 'pref':
1529										$mask = (1 << (Indic::PREF));
1530										break;
1531									case 'blwf':
1532										$mask = (1 << (Indic::BLWF));
1533										break;
1534									case 'pstf':
1535										$mask = (1 << (Indic::PSTF));
1536										break;
1537								}
1538								if (!($this->OTLdata[$ptr]['mask'] & $mask)) {
1539									continue;
1540								}
1541
1542								$nextGlyph = $this->OTLdata[$ptr + 1]['hex'];
1543								$nextGID = $this->OTLdata[$ptr + 1]['uni'];
1544								if (isset($this->GSLuCoverage[$lu][$c][$nextGID])) {
1545									// Get rules from font GSUB subtable
1546									$shift = $this->_applyGSUBsubtableSpecial($lu, $c, $ptr, $currGlyph, $currGID, $nextGlyph, $nextGID, ($subtable_offset - $this->GSUB_offset), $Type, $this->GSLuCoverage[$lu][$c]);
1547
1548									if ($shift) {
1549										break;
1550									}
1551								}
1552							}
1553						}
1554					}
1555					if ($shift == 0) {
1556						$shift = 1;
1557					}
1558					$ptr += $shift;
1559				}
1560			}
1561		}
1562	}
1563
1564	function _applyGSUBsubtableSpecial($lookupID, $subtable, $ptr, $currGlyph, $currGID, $nextGlyph, $nextGID, $subtable_offset, $Type, $LuCoverage)
1565	{
1566
1567		// Special case for Indic
1568		// Check to substitute Halant-Consonant in PREF, BLWF or PSTF
1569		// i.e. new spec but GSUB tables have Consonant-Halant in Lookups e.g. FreeSerif, which
1570		// incorrectly just moved old spec tables to new spec. Uniscribe seems to cope with this
1571		// See also ttffontsuni.php
1572
1573		$this->seek($subtable_offset);
1574		$SubstFormat = $this->read_ushort();
1575
1576		// Subtable contains Consonant - Halant
1577		// Text string contains Halant ($CurrGlyph) - Consonant ($nextGlyph)
1578		// Halant has already been matched, and already checked that $nextGID is in Coverage table
1579		////////////////////////////////////////////////////////////////////////////////
1580		// Only does: LookupType 4: Ligature Substitution Subtable : n to 1
1581		////////////////////////////////////////////////////////////////////////////////
1582		$Coverage = $subtable_offset + $this->read_ushort();
1583		$NextGlyphPos = $LuCoverage[$nextGID];
1584		$LigSetCount = $this->read_short();
1585
1586		$this->skip($NextGlyphPos * 2);
1587		$LigSet = $subtable_offset + $this->read_short();
1588
1589		$this->seek($LigSet);
1590		$LigCount = $this->read_short();
1591		// LigatureSet i.e. all starting with the same Glyph $nextGlyph [Consonant]
1592		$LigatureOffset = [];
1593		for ($g = 0; $g < $LigCount; $g++) {
1594			$LigatureOffset[$g] = $LigSet + $this->read_ushort();
1595		}
1596		for ($g = 0; $g < $LigCount; $g++) {
1597			// Ligature tables
1598			$this->seek($LigatureOffset[$g]);
1599			$LigGlyph = $this->read_ushort();
1600			$substitute = $this->glyphToChar($LigGlyph);
1601			$CompCount = $this->read_ushort();
1602
1603			if ($CompCount != 2) {
1604				return 0;
1605			} // Only expecting to work with 2:1 (and no ignore characters in between)
1606
1607
1608			$gid = $this->read_ushort();
1609			$checkGlyph = $this->glyphToChar($gid); // Other component/input Glyphs starting at position 2 (arrayindex 1)
1610
1611			if ($currGID == $checkGlyph) {
1612				$match = true;
1613			} else {
1614				$match = false;
1615				break;
1616			}
1617
1618			$GlyphPos = [];
1619			$GlyphPos[] = $ptr;
1620			$GlyphPos[] = $ptr + 1;
1621
1622
1623			if ($match) {
1624				$shift = $this->GSUBsubstitute($ptr, $substitute, 4, $GlyphPos); // GlyphPos contains positions to set null
1625				if ($shift) {
1626					return 1;
1627				}
1628			}
1629		}
1630		return 0;
1631	}
1632
1633	function _applyGSUBsubtable($lookupID, $subtable, $ptr, $currGlyph, $currGID, $subtable_offset, $Type, $Flag, $MarkFilteringSet, $LuCoverage, $level, $currentTag, $is_old_spec, $tagInt)
1634	{
1635		$ignore = $this->_getGCOMignoreString($Flag, $MarkFilteringSet);
1636
1637		// Lets start
1638		$this->seek($subtable_offset);
1639		$SubstFormat = $this->read_ushort();
1640
1641		////////////////////////////////////////////////////////////////////////////////
1642		// LookupType 1: Single Substitution Subtable : 1 to 1
1643		////////////////////////////////////////////////////////////////////////////////
1644		if ($Type == 1) {
1645			// Flag = Ignore
1646			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1647				return 0;
1648			}
1649			$CoverageOffset = $subtable_offset + $this->read_ushort();
1650			$GlyphPos = $LuCoverage[$currGID];
1651			//===========
1652			// Format 1:
1653			//===========
1654			if ($SubstFormat == 1) { // Calculated output glyph indices
1655				$DeltaGlyphID = $this->read_short();
1656				$this->seek($CoverageOffset);
1657				$glyphs = $this->_getCoverageGID();
1658				$GlyphID = $glyphs[$GlyphPos] + $DeltaGlyphID;
1659			} //===========
1660			// Format 2:
1661			//===========
1662			elseif ($SubstFormat == 2) { // Specified output glyph indices
1663				$GlyphCount = $this->read_ushort();
1664				$this->skip($GlyphPos * 2);
1665				$GlyphID = $this->read_ushort();
1666			}
1667
1668			$substitute = $this->glyphToChar($GlyphID);
1669			$shift = $this->GSUBsubstitute($ptr, $substitute, $Type);
1670			if ($this->debugOTL && $shift) {
1671				$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1672			}
1673			if ($shift) {
1674				return 1;
1675			}
1676			return 0;
1677		} ////////////////////////////////////////////////////////////////////////////////
1678		// LookupType 2: Multiple Substitution Subtable : 1 to n
1679		////////////////////////////////////////////////////////////////////////////////
1680		elseif ($Type == 2) {
1681			// Flag = Ignore
1682			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1683				return 0;
1684			}
1685			$Coverage = $subtable_offset + $this->read_ushort();
1686			$GlyphPos = $LuCoverage[$currGID];
1687			$this->skip(2);
1688			$this->skip($GlyphPos * 2);
1689			$Sequences = $subtable_offset + $this->read_short();
1690
1691			$this->seek($Sequences);
1692			$GlyphCount = $this->read_short();
1693			$SubstituteGlyphs = [];
1694			for ($g = 0; $g < $GlyphCount; $g++) {
1695				$sgid = $this->read_ushort();
1696				$SubstituteGlyphs[] = $this->glyphToChar($sgid);
1697			}
1698
1699			$shift = $this->GSUBsubstitute($ptr, $SubstituteGlyphs, $Type);
1700			if ($this->debugOTL && $shift) {
1701				$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1702			}
1703			if ($shift) {
1704				return $shift;
1705			}
1706			return 0;
1707		} ////////////////////////////////////////////////////////////////////////////////
1708		// LookupType 3: Alternate Forms : 1 to 1(n)
1709		////////////////////////////////////////////////////////////////////////////////
1710		elseif ($Type == 3) {
1711			// Flag = Ignore
1712			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1713				return 0;
1714			}
1715			$Coverage = $subtable_offset + $this->read_ushort();
1716			$AlternateSetCount = $this->read_short();
1717			///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1718			// Need to set alternate IF set by CSS3 font-feature for a tag
1719			// i.e. if this is 'salt' alternate may be set to 2
1720			// default value will be $alt=1 ( === index of 0 in list of alternates)
1721			$alt = 1; // $alt=1 points to Alternative[0]
1722			if ($tagInt > 1) {
1723				$alt = $tagInt;
1724			}
1725			///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1726			if ($alt == 0) {
1727				return 0;
1728			} // If specified alternate not present, cancel [ or could default $alt = 1 ?]
1729
1730			$GlyphPos = $LuCoverage[$currGID];
1731			$this->skip($GlyphPos * 2);
1732
1733			$AlternateSets = $subtable_offset + $this->read_short();
1734			$this->seek($AlternateSets);
1735
1736			$AlternateGlyphCount = $this->read_short();
1737			if ($alt > $AlternateGlyphCount) {
1738				return 0;
1739			} // If specified alternate not present, cancel [ or could default $alt = 1 ?]
1740
1741			$this->skip(($alt - 1) * 2);
1742			$GlyphID = $this->read_ushort();
1743
1744			$substitute = $this->glyphToChar($GlyphID);
1745			$shift = $this->GSUBsubstitute($ptr, $substitute, $Type);
1746			if ($this->debugOTL && $shift) {
1747				$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1748			}
1749			if ($shift) {
1750				return 1;
1751			}
1752			return 0;
1753		} ////////////////////////////////////////////////////////////////////////////////
1754		// LookupType 4: Ligature Substitution Subtable : n to 1
1755		////////////////////////////////////////////////////////////////////////////////
1756		elseif ($Type == 4) {
1757			// Flag = Ignore
1758			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1759				return 0;
1760			}
1761			$Coverage = $subtable_offset + $this->read_ushort();
1762			$FirstGlyphPos = $LuCoverage[$currGID];
1763
1764			$LigSetCount = $this->read_short();
1765
1766			$this->skip($FirstGlyphPos * 2);
1767			$LigSet = $subtable_offset + $this->read_short();
1768
1769			$this->seek($LigSet);
1770			$LigCount = $this->read_short();
1771			// LigatureSet i.e. all starting with the same first Glyph $currGlyph
1772			$LigatureOffset = [];
1773			for ($g = 0; $g < $LigCount; $g++) {
1774				$LigatureOffset[$g] = $LigSet + $this->read_ushort();
1775			}
1776			for ($g = 0; $g < $LigCount; $g++) {
1777				// Ligature tables
1778				$this->seek($LigatureOffset[$g]);
1779				$LigGlyph = $this->read_ushort(); // Output Ligature GlyphID
1780				$substitute = $this->glyphToChar($LigGlyph);
1781				$CompCount = $this->read_ushort();
1782
1783				$spos = $ptr;
1784				$match = true;
1785				$GlyphPos = [];
1786				$GlyphPos[] = $spos;
1787				for ($l = 1; $l < $CompCount; $l++) {
1788					$gid = $this->read_ushort();
1789					$checkGlyph = $this->glyphToChar($gid); // Other component/input Glyphs starting at position 2 (arrayindex 1)
1790
1791					$spos++;
1792					//while $this->OTLdata[$spos]['uni'] is an "ignore" =>  spos++
1793					while (isset($this->OTLdata[$spos]) && strpos($ignore, $this->OTLdata[$spos]['hex']) !== false) {
1794						$spos++;
1795					}
1796
1797					if (isset($this->OTLdata[$spos]) && $this->OTLdata[$spos]['uni'] == $checkGlyph) {
1798						$GlyphPos[] = $spos;
1799					} else {
1800						$match = false;
1801						break;
1802					}
1803				}
1804
1805
1806				if ($match) {
1807					$shift = $this->GSUBsubstitute($ptr, $substitute, $Type, $GlyphPos); // GlyphPos contains positions to set null
1808					if ($this->debugOTL && $shift) {
1809						$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1810					}
1811					if ($shift) {
1812						return ($spos - $ptr + 1 - ($CompCount - 1));
1813					}
1814				}
1815			}
1816			return 0;
1817		} ////////////////////////////////////////////////////////////////////////////////
1818		// LookupType 5: Contextual Substitution Subtable
1819		////////////////////////////////////////////////////////////////////////////////
1820		elseif ($Type == 5) {
1821			//===========
1822			// Format 1: Simple Context Glyph Substitution
1823			//===========
1824			if ($SubstFormat == 1) {
1825				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
1826				$SubRuleSetCount = $this->read_ushort();
1827				$SubRuleSetOffset = [];
1828				for ($b = 0; $b < $SubRuleSetCount; $b++) {
1829					$offset = $this->read_ushort();
1830					if ($offset == 0x0000) {
1831						$SubRuleSetOffset[] = $offset;
1832					} else {
1833						$SubRuleSetOffset[] = $subtable_offset + $offset;
1834					}
1835				}
1836
1837				// SubRuleSet tables: All contexts beginning with the same glyph
1838				// Select the SubRuleSet required using the position of the glyph in the coverage table
1839				$GlyphPos = $LuCoverage[$currGID];
1840				if ($SubRuleSetOffset[$GlyphPos] > 0) {
1841					$this->seek($SubRuleSetOffset[$GlyphPos]);
1842					$SubRuleCnt = $this->read_ushort();
1843					$SubRule = [];
1844					for ($b = 0; $b < $SubRuleCnt; $b++) {
1845						$SubRule[$b] = $SubRuleSetOffset[$GlyphPos] + $this->read_ushort();
1846					}
1847					for ($b = 0; $b < $SubRuleCnt; $b++) {  // EACH RULE
1848						$this->seek($SubRule[$b]);
1849						$InputGlyphCount = $this->read_ushort();
1850						$SubstCount = $this->read_ushort();
1851
1852						$Backtrack = [];
1853						$Lookahead = [];
1854						$Input = [];
1855						$Input[0] = $this->OTLdata[$ptr]['uni'];
1856						for ($r = 1; $r < $InputGlyphCount; $r++) {
1857							$gid = $this->read_ushort();
1858							$Input[$r] = $this->glyphToChar($gid);
1859						}
1860						$matched = $this->checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr);
1861						if ($matched) {
1862							if ($this->debugOTL) {
1863								$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1864							}
1865							for ($p = 0; $p < $SubstCount; $p++) { // EACH LOOKUP
1866								$SequenceIndex[$p] = $this->read_ushort();
1867								$LookupListIndex[$p] = $this->read_ushort();
1868							}
1869
1870							for ($p = 0; $p < $SubstCount; $p++) {
1871								// Apply  $LookupListIndex  at   $SequenceIndex
1872								if ($SequenceIndex[$p] >= $InputGlyphCount) {
1873									continue;
1874								}
1875								$lu = $LookupListIndex[$p];
1876								$luType = $this->GSUBLookups[$lu]['Type'];
1877								$luFlag = $this->GSUBLookups[$lu]['Flag'];
1878								$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1879
1880								$luptr = $matched[$SequenceIndex[$p]];
1881								$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1882								$lucurrGID = $this->OTLdata[$luptr]['uni'];
1883
1884								foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
1885									$shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
1886									if ($shift) {
1887										break;
1888									}
1889								}
1890							}
1891
1892							if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
1893								return $shift;
1894							} /* OTL_FIX_3 */
1895							else {
1896								return $InputGlyphCount; // should be + matched ignores in Input Sequence
1897							}
1898						}
1899					}
1900				}
1901				return 0;
1902			} //===========
1903			// Format 2:
1904			//===========
1905			// Format 2: Class-based Context Glyph Substitution
1906			elseif ($SubstFormat == 2) {
1907				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
1908				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
1909				$SubClassSetCnt = $this->read_ushort();
1910				$SubClassSetOffset = [];
1911				for ($b = 0; $b < $SubClassSetCnt; $b++) {
1912					$offset = $this->read_ushort();
1913					if ($offset == 0x0000) {
1914						$SubClassSetOffset[] = $offset;
1915					} else {
1916						$SubClassSetOffset[] = $subtable_offset + $offset;
1917					}
1918				}
1919
1920				$InputClasses = $this->_getClasses($InputClassDefOffset);
1921
1922				for ($s = 0; $s < $SubClassSetCnt; $s++) { // $SubClassSet is ordered by input class-may be NULL
1923					// Select $SubClassSet if currGlyph is in First Input Class
1924					if ($SubClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
1925						$this->seek($SubClassSetOffset[$s]);
1926						$SubClassRuleCnt = $this->read_ushort();
1927						$SubClassRule = [];
1928						for ($b = 0; $b < $SubClassRuleCnt; $b++) {
1929							$SubClassRule[$b] = $SubClassSetOffset[$s] + $this->read_ushort();
1930						}
1931
1932						for ($b = 0; $b < $SubClassRuleCnt; $b++) {  // EACH RULE
1933							$this->seek($SubClassRule[$b]);
1934							$InputGlyphCount = $this->read_ushort();
1935							$SubstCount = $this->read_ushort();
1936							$Input = [];
1937							for ($r = 1; $r < $InputGlyphCount; $r++) {
1938								$Input[$r] = $this->read_ushort();
1939							}
1940
1941							$inputClass = $s;
1942
1943							$inputGlyphs = [];
1944							$inputGlyphs[0] = $InputClasses[$inputClass];
1945
1946							if ($InputGlyphCount > 1) {
1947								//  NB starts at 1
1948								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
1949									$classindex = $Input[$gcl];
1950									if (isset($InputClasses[$classindex])) {
1951										$inputGlyphs[$gcl] = $InputClasses[$classindex];
1952									} else {
1953										$inputGlyphs[$gcl] = '';
1954									}
1955								}
1956							}
1957
1958							// Class 0 contains all the glyphs NOT in the other classes
1959							$class0excl = [];
1960							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
1961								if (is_array($InputClasses[$gc])) {
1962									$class0excl = $class0excl + $InputClasses[$gc];
1963								}
1964							}
1965
1966							$backtrackGlyphs = [];
1967							$lookaheadGlyphs = [];
1968
1969							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl);
1970							if ($matched) {
1971								if ($this->debugOTL) {
1972									$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1973								}
1974								for ($p = 0; $p < $SubstCount; $p++) { // EACH LOOKUP
1975									$SequenceIndex[$p] = $this->read_ushort();
1976									$LookupListIndex[$p] = $this->read_ushort();
1977								}
1978
1979								for ($p = 0; $p < $SubstCount; $p++) {
1980									// Apply  $LookupListIndex  at   $SequenceIndex
1981									if ($SequenceIndex[$p] >= $InputGlyphCount) {
1982										continue;
1983									}
1984									$lu = $LookupListIndex[$p];
1985									$luType = $this->GSUBLookups[$lu]['Type'];
1986									$luFlag = $this->GSUBLookups[$lu]['Flag'];
1987									$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1988
1989									$luptr = $matched[$SequenceIndex[$p]];
1990									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1991									$lucurrGID = $this->OTLdata[$luptr]['uni'];
1992
1993									foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
1994										$shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
1995										if ($shift) {
1996											break;
1997										}
1998									}
1999								}
2000
2001								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2002									return $shift;
2003								} /* OTL_FIX_3 */
2004								else {
2005									return $InputGlyphCount; // should be + matched ignores in Input Sequence
2006								}
2007							}
2008						}
2009					}
2010				}
2011
2012				return 0;
2013			} //===========
2014			// Format 3:
2015			//===========
2016			// Format 3: Coverage-based Context Glyph Substitution
2017			elseif ($SubstFormat == 3) {
2018				throw new \Mpdf\MpdfException("GSUB Lookup Type " . $Type . " Format " . $SubstFormat . " not TESTED YET.");
2019			}
2020		} ////////////////////////////////////////////////////////////////////////////////
2021		// LookupType 6: Chaining Contextual Substitution Subtable
2022		////////////////////////////////////////////////////////////////////////////////
2023		elseif ($Type == 6) {
2024			//===========
2025			// Format 1:
2026			//===========
2027			// Format 1: Simple Chaining Context Glyph Substitution
2028			if ($SubstFormat == 1) {
2029				$Coverage = $subtable_offset + $this->read_ushort();
2030				$GlyphPos = $LuCoverage[$currGID];
2031				$ChainSubRuleSetCount = $this->read_ushort();
2032				// All of the ChainSubRule tables defining contexts that begin with the same first glyph are grouped together and defined in a ChainSubRuleSet table
2033				$this->skip($GlyphPos * 2);
2034				$ChainSubRuleSet = $subtable_offset + $this->read_ushort();
2035				$this->seek($ChainSubRuleSet);
2036				$ChainSubRuleCount = $this->read_ushort();
2037
2038				for ($s = 0; $s < $ChainSubRuleCount; $s++) {
2039					$ChainSubRule[$s] = $ChainSubRuleSet + $this->read_ushort();
2040				}
2041
2042				for ($s = 0; $s < $ChainSubRuleCount; $s++) {
2043					$this->seek($ChainSubRule[$s]);
2044
2045					$BacktrackGlyphCount = $this->read_ushort();
2046					$Backtrack = [];
2047					for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
2048						$gid = $this->read_ushort();
2049						$Backtrack[] = $this->glyphToChar($gid);
2050					}
2051					$Input = [];
2052					$Input[0] = $this->OTLdata[$ptr]['uni'];
2053					$InputGlyphCount = $this->read_ushort();
2054					for ($b = 1; $b < $InputGlyphCount; $b++) {
2055						$gid = $this->read_ushort();
2056						$Input[$b] = $this->glyphToChar($gid);
2057					}
2058					$LookaheadGlyphCount = $this->read_ushort();
2059					$Lookahead = [];
2060					for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
2061						$gid = $this->read_ushort();
2062						$Lookahead[] = $this->glyphToChar($gid);
2063					}
2064
2065					$matched = $this->checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr);
2066					if ($matched) {
2067						if ($this->debugOTL) {
2068							$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
2069						}
2070						$SubstCount = $this->read_ushort();
2071						for ($p = 0; $p < $SubstCount; $p++) {
2072							// SubstLookupRecord
2073							$SubstLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
2074							$SubstLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
2075						}
2076						for ($p = 0; $p < $SubstCount; $p++) {
2077							// Apply  $SubstLookupRecord[$p]['LookupListIndex']  at   $SubstLookupRecord[$p]['SequenceIndex']
2078							if ($SubstLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) {
2079								continue;
2080							}
2081							$lu = $SubstLookupRecord[$p]['LookupListIndex'];
2082							$luType = $this->GSUBLookups[$lu]['Type'];
2083							$luFlag = $this->GSUBLookups[$lu]['Flag'];
2084							$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2085
2086							$luptr = $matched[$SubstLookupRecord[$p]['SequenceIndex']];
2087							$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2088							$lucurrGID = $this->OTLdata[$luptr]['uni'];
2089
2090							foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
2091								$shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
2092								if ($shift) {
2093									break;
2094								}
2095							}
2096						}
2097						if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2098							return $shift;
2099						} /* OTL_FIX_3 */
2100						else {
2101							return $InputGlyphCount; // should be + matched ignores in Input Sequence
2102						}
2103					}
2104				}
2105				return 0;
2106			} //===========
2107			// Format 2:
2108			//===========
2109			// Format 2: Class-based Chaining Context Glyph Substitution  p257
2110			elseif ($SubstFormat == 2) {
2111				// NB Format 2 specifies fixed class assignments (identical for each position in the backtrack, input, or lookahead sequence) and exclusive classes (a glyph cannot be in more than one class at a time)
2112
2113				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
2114				$BacktrackClassDefOffset = $subtable_offset + $this->read_ushort();
2115				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
2116				$LookaheadClassDefOffset = $subtable_offset + $this->read_ushort();
2117				$ChainSubClassSetCnt = $this->read_ushort();
2118				$ChainSubClassSetOffset = [];
2119				for ($b = 0; $b < $ChainSubClassSetCnt; $b++) {
2120					$offset = $this->read_ushort();
2121					if ($offset == 0x0000) {
2122						$ChainSubClassSetOffset[] = $offset;
2123					} else {
2124						$ChainSubClassSetOffset[] = $subtable_offset + $offset;
2125					}
2126				}
2127
2128				$BacktrackClasses = $this->_getClasses($BacktrackClassDefOffset);
2129				$InputClasses = $this->_getClasses($InputClassDefOffset);
2130				$LookaheadClasses = $this->_getClasses($LookaheadClassDefOffset);
2131
2132				for ($s = 0; $s < $ChainSubClassSetCnt; $s++) { // $ChainSubClassSet is ordered by input class-may be NULL
2133					// Select $ChainSubClassSet if currGlyph is in First Input Class
2134					if ($ChainSubClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
2135						$this->seek($ChainSubClassSetOffset[$s]);
2136						$ChainSubClassRuleCnt = $this->read_ushort();
2137						$ChainSubClassRule = [];
2138						for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {
2139							$ChainSubClassRule[$b] = $ChainSubClassSetOffset[$s] + $this->read_ushort();
2140						}
2141
2142						for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {  // EACH RULE
2143							$this->seek($ChainSubClassRule[$b]);
2144							$BacktrackGlyphCount = $this->read_ushort();
2145							for ($r = 0; $r < $BacktrackGlyphCount; $r++) {
2146								$Backtrack[$r] = $this->read_ushort();
2147							}
2148							$InputGlyphCount = $this->read_ushort();
2149							for ($r = 1; $r < $InputGlyphCount; $r++) {
2150								$Input[$r] = $this->read_ushort();
2151							}
2152							$LookaheadGlyphCount = $this->read_ushort();
2153							for ($r = 0; $r < $LookaheadGlyphCount; $r++) {
2154								$Lookahead[$r] = $this->read_ushort();
2155							}
2156
2157
2158							// These contain classes of glyphs as arrays
2159							// $InputClasses[(class)] e.g. 0x02E6,0x02E7,0x02E8
2160							// $LookaheadClasses[(class)]
2161							// $BacktrackClasses[(class)]
2162							// These contain arrays of classIndexes
2163							// [Backtrack] [Lookahead] and [Input] (Input is from the second position only)
2164
2165
2166							$inputClass = $s; //???
2167
2168							$inputGlyphs = [];
2169							$inputGlyphs[0] = $InputClasses[$inputClass];
2170
2171							if ($InputGlyphCount > 1) {
2172								//  NB starts at 1
2173								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
2174									$classindex = $Input[$gcl];
2175									if (isset($InputClasses[$classindex])) {
2176										$inputGlyphs[$gcl] = $InputClasses[$classindex];
2177									} else {
2178										$inputGlyphs[$gcl] = '';
2179									}
2180								}
2181							}
2182
2183							// Class 0 contains all the glyphs NOT in the other classes
2184							$class0excl = [];
2185							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
2186								if (isset($InputClasses[$gc])) {
2187									$class0excl = $class0excl + $InputClasses[$gc];
2188								}
2189							}
2190
2191							if ($BacktrackGlyphCount) {
2192								for ($gcl = 0; $gcl < $BacktrackGlyphCount; $gcl++) {
2193									$classindex = $Backtrack[$gcl];
2194									if (isset($BacktrackClasses[$classindex])) {
2195										$backtrackGlyphs[$gcl] = $BacktrackClasses[$classindex];
2196									} else {
2197										$backtrackGlyphs[$gcl] = '';
2198									}
2199								}
2200							} else {
2201								$backtrackGlyphs = [];
2202							}
2203
2204							// Class 0 contains all the glyphs NOT in the other classes
2205							$bclass0excl = [];
2206							for ($gc = 1; $gc <= count($BacktrackClasses); $gc++) {
2207								if (isset($BacktrackClasses[$gc])) {
2208									$bclass0excl = $bclass0excl + $BacktrackClasses[$gc];
2209								}
2210							}
2211
2212
2213							if ($LookaheadGlyphCount) {
2214								for ($gcl = 0; $gcl < $LookaheadGlyphCount; $gcl++) {
2215									$classindex = $Lookahead[$gcl];
2216									if (isset($LookaheadClasses[$classindex])) {
2217										$lookaheadGlyphs[$gcl] = $LookaheadClasses[$classindex];
2218									} else {
2219										$lookaheadGlyphs[$gcl] = '';
2220									}
2221								}
2222							} else {
2223								$lookaheadGlyphs = [];
2224							}
2225
2226							// Class 0 contains all the glyphs NOT in the other classes
2227							$lclass0excl = [];
2228							for ($gc = 1; $gc <= count($LookaheadClasses); $gc++) {
2229								if (isset($LookaheadClasses[$gc])) {
2230									$lclass0excl = $lclass0excl + $LookaheadClasses[$gc];
2231								}
2232							}
2233
2234
2235							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl, $bclass0excl, $lclass0excl);
2236							if ($matched) {
2237								if ($this->debugOTL) {
2238									$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
2239								}
2240								$SubstCount = $this->read_ushort();
2241								for ($p = 0; $p < $SubstCount; $p++) { // EACH LOOKUP
2242									$SequenceIndex[$p] = $this->read_ushort();
2243									$LookupListIndex[$p] = $this->read_ushort();
2244								}
2245
2246								for ($p = 0; $p < $SubstCount; $p++) {
2247									// Apply  $LookupListIndex  at   $SequenceIndex
2248									if ($SequenceIndex[$p] >= $InputGlyphCount) {
2249										continue;
2250									}
2251									$lu = $LookupListIndex[$p];
2252									$luType = $this->GSUBLookups[$lu]['Type'];
2253									$luFlag = $this->GSUBLookups[$lu]['Flag'];
2254									$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2255
2256									$luptr = $matched[$SequenceIndex[$p]];
2257									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2258									$lucurrGID = $this->OTLdata[$luptr]['uni'];
2259
2260									foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
2261										$shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
2262										if ($shift) {
2263											break;
2264										}
2265									}
2266								}
2267
2268								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2269									return $shift;
2270								} /* OTL_FIX_3 */
2271								else {
2272									return $InputGlyphCount; // should be + matched ignores in Input Sequence
2273								}
2274							}
2275						}
2276					}
2277				}
2278
2279				return 0;
2280			} //===========
2281			// Format 3:
2282			//===========
2283			// Format 3: Coverage-based Chaining Context Glyph Substitution  p259
2284			elseif ($SubstFormat == 3) {
2285				$BacktrackGlyphCount = $this->read_ushort();
2286				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
2287					$CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2288				}
2289				$InputGlyphCount = $this->read_ushort();
2290				for ($b = 0; $b < $InputGlyphCount; $b++) {
2291					$CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2292				}
2293				$LookaheadGlyphCount = $this->read_ushort();
2294				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
2295					$CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2296				}
2297				$SubstCount = $this->read_ushort();
2298				$save_pos = $this->_pos; // Save the point just after PosCount
2299
2300				$CoverageBacktrackGlyphs = [];
2301				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
2302					$this->seek($CoverageBacktrackOffset[$b]);
2303					$glyphs = $this->_getCoverage();
2304					$CoverageBacktrackGlyphs[$b] = implode("|", $glyphs);
2305				}
2306				$CoverageInputGlyphs = [];
2307				for ($b = 0; $b < $InputGlyphCount; $b++) {
2308					$this->seek($CoverageInputOffset[$b]);
2309					$glyphs = $this->_getCoverage();
2310					$CoverageInputGlyphs[$b] = implode("|", $glyphs);
2311				}
2312				$CoverageLookaheadGlyphs = [];
2313				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
2314					$this->seek($CoverageLookaheadOffset[$b]);
2315					$glyphs = $this->_getCoverage();
2316					$CoverageLookaheadGlyphs[$b] = implode("|", $glyphs);
2317				}
2318
2319				$matched = $this->checkContextMatchMultiple($CoverageInputGlyphs, $CoverageBacktrackGlyphs, $CoverageLookaheadGlyphs, $ignore, $ptr);
2320				if ($matched) {
2321					if ($this->debugOTL) {
2322						$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
2323					}
2324
2325					$this->seek($save_pos); // Return to just after PosCount
2326					for ($p = 0; $p < $SubstCount; $p++) {
2327						// SubstLookupRecord
2328						$SubstLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
2329						$SubstLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
2330					}
2331					for ($p = 0; $p < $SubstCount; $p++) {
2332						// Apply  $SubstLookupRecord[$p]['LookupListIndex']  at   $SubstLookupRecord[$p]['SequenceIndex']
2333						if ($SubstLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) {
2334							continue;
2335						}
2336						$lu = $SubstLookupRecord[$p]['LookupListIndex'];
2337						$luType = $this->GSUBLookups[$lu]['Type'];
2338						$luFlag = $this->GSUBLookups[$lu]['Flag'];
2339						$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2340
2341						$luptr = $matched[$SubstLookupRecord[$p]['SequenceIndex']];
2342						$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2343						$lucurrGID = $this->OTLdata[$luptr]['uni'];
2344
2345						foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
2346							$shift = $this->_applyGSUBsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GSUB_offset), $luType, $luFlag, $luMarkFilteringSet, $this->GSLuCoverage[$lu][$luc], 1, $currentTag, $is_old_spec, $tagInt);
2347							if ($shift) {
2348								break;
2349							}
2350						}
2351					}
2352					if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2353						return (isset($shift) ? $shift : 0);
2354					} /* OTL_FIX_3 */
2355					else {
2356						return $InputGlyphCount; // should be + matched ignores in Input Sequence
2357					}
2358				}
2359
2360				return 0;
2361			}
2362		} else {
2363			throw new \Mpdf\MpdfException("GSUB Lookup Type " . $Type . " not supported.");
2364		}
2365	}
2366
2367	function _updateLigatureMarks($pos, $n)
2368	{
2369		if ($n > 0) {
2370			// Update position of Ligatures and associated Marks
2371			// Foreach lig/assocMarks
2372			// Any position lpos or mpos > $pos + count($substitute)
2373			//  $this->assocMarks = array();    // assocMarks[$pos mpos] => array(compID, ligPos)
2374			//  $this->assocLigs = array(); // Ligatures[$pos lpos] => nc
2375			for ($p = count($this->OTLdata) - 1; $p >= ($pos + $n); $p--) {
2376				if (isset($this->assocLigs[$p])) {
2377					$tmp = $this->assocLigs[$p];
2378					unset($this->assocLigs[$p]);
2379					$this->assocLigs[($p + $n)] = $tmp;
2380				}
2381			}
2382			for ($p = count($this->OTLdata) - 1; $p >= 0; $p--) {
2383				if (isset($this->assocMarks[$p])) {
2384					if ($this->assocMarks[$p]['ligPos'] >= ($pos + $n)) {
2385						$this->assocMarks[$p]['ligPos'] += $n;
2386					}
2387					if ($p >= ($pos + $n)) {
2388						$tmp = $this->assocMarks[$p];
2389						unset($this->assocMarks[$p]);
2390						$this->assocMarks[($p + $n)] = $tmp;
2391					}
2392				}
2393			}
2394		} elseif ($n < 1) { // glyphs removed
2395			$nrem = -$n;
2396			// Update position of pre-existing Ligatures and associated Marks
2397			for ($p = ($pos + 1); $p < count($this->OTLdata); $p++) {
2398				if (isset($this->assocLigs[$p])) {
2399					$tmp = $this->assocLigs[$p];
2400					unset($this->assocLigs[$p]);
2401					$this->assocLigs[($p - $nrem)] = $tmp;
2402				}
2403			}
2404			for ($p = 0; $p < count($this->OTLdata); $p++) {
2405				if (isset($this->assocMarks[$p])) {
2406					if ($this->assocMarks[$p]['ligPos'] >= ($pos)) {
2407						$this->assocMarks[$p]['ligPos'] -= $nrem;
2408					}
2409					if ($p > $pos) {
2410						$tmp = $this->assocMarks[$p];
2411						unset($this->assocMarks[$p]);
2412						$this->assocMarks[($p - $nrem)] = $tmp;
2413					}
2414				}
2415			}
2416		}
2417	}
2418
2419	function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null)
2420	{
2421
2422		// LookupType 1: Simple Substitution Subtable : 1 to 1
2423		// LookupType 3: Alternate Forms : 1 to 1(n)
2424		if ($Type == 1 || $Type == 3) {
2425			$this->OTLdata[$pos]['uni'] = $substitute;
2426			$this->OTLdata[$pos]['hex'] = $this->unicode_hex($substitute);
2427			return 1;
2428		} // LookupType 2: Multiple Substitution Subtable : 1 to n
2429		elseif ($Type == 2) {
2430			for ($i = 0; $i < count($substitute); $i++) {
2431				$uni = $substitute[$i];
2432				$newOTLdata[$i] = [];
2433				$newOTLdata[$i]['uni'] = $uni;
2434				$newOTLdata[$i]['hex'] = $this->unicode_hex($uni);
2435
2436
2437				// Get types of new inserted chars - or replicate type of char being replaced
2438				//  $bt = Ucdn::get_bidi_class($uni);
2439				//  if (!$bt) {
2440				$bt = $this->OTLdata[$pos]['bidi_type'];
2441				//  }
2442
2443				if (strpos($this->GlyphClassMarks, $newOTLdata[$i]['hex']) !== false) {
2444					$gp = 'M';
2445				} elseif ($uni == 32) {
2446					$gp = 'S';
2447				} else {
2448					$gp = 'C';
2449				}
2450
2451				// Need to update matra_type ??? of new glyphs inserted ???????????????????????????????????????
2452
2453				$newOTLdata[$i]['bidi_type'] = $bt;
2454				$newOTLdata[$i]['group'] = $gp;
2455
2456				// Need to update details of new glyphs inserted
2457				$newOTLdata[$i]['general_category'] = $this->OTLdata[$pos]['general_category'];
2458
2459				if ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
2460					$newOTLdata[$i]['indic_category'] = $this->OTLdata[$pos]['indic_category'];
2461					$newOTLdata[$i]['indic_position'] = $this->OTLdata[$pos]['indic_position'];
2462				} elseif ($this->shaper == 'M') {
2463					$newOTLdata[$i]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category'];
2464					$newOTLdata[$i]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position'];
2465				}
2466				if (isset($this->OTLdata[$pos]['mask'])) {
2467					$newOTLdata[$i]['mask'] = $this->OTLdata[$pos]['mask'];
2468				}
2469				if (isset($this->OTLdata[$pos]['syllable'])) {
2470					$newOTLdata[$i]['syllable'] = $this->OTLdata[$pos]['syllable'];
2471				}
2472			}
2473			if ($this->shaper == 'K' || $this->shaper == 'T' || $this->shaper == 'L') {
2474				if ($this->OTLdata[$pos]['wordend']) {
2475					$newOTLdata[count($substitute) - 1]['wordend'] = true;
2476				}
2477			}
2478
2479			array_splice($this->OTLdata, $pos, 1, $newOTLdata); // Replace 1 with n
2480			// Update position of Ligatures and associated Marks
2481			// count($substitute)-1  is the number of glyphs added
2482			$nadd = count($substitute) - 1;
2483			$this->_updateLigatureMarks($pos, $nadd);
2484			return count($substitute);
2485		} // LookupType 4: Ligature Substitution Subtable : n to 1
2486		elseif ($Type == 4) {
2487			// Create Ligatures and associated Marks
2488			$firstGlyph = $this->OTLdata[$pos]['hex'];
2489
2490			// If all components of the ligature are marks (and in the same syllable), we call this a mark ligature.
2491			$contains_marks = false;
2492			$contains_nonmarks = false;
2493			if (isset($this->OTLdata[$pos]['syllable'])) {
2494				$current_syllable = $this->OTLdata[$pos]['syllable'];
2495			} else {
2496				$current_syllable = 0;
2497			}
2498			for ($i = 0; $i < count($GlyphPos); $i++) {
2499				// If subsequent components are not Marks as well - don't ligate
2500				$unistr = $this->OTLdata[$GlyphPos[$i]]['hex'];
2501				if ($this->restrictToSyllable && isset($this->OTLdata[$GlyphPos[$i]]['syllable']) && $this->OTLdata[$GlyphPos[$i]]['syllable'] != $current_syllable) {
2502					return 0;
2503				}
2504				if (strpos($this->GlyphClassMarks, $unistr) !== false) {
2505					$contains_marks = true;
2506				} else {
2507					$contains_nonmarks = true;
2508				}
2509			}
2510			if ($contains_marks && !$contains_nonmarks) {
2511				// Mark Ligature (all components are Marks)
2512				$firstMarkAssoc = '';
2513				if (isset($this->assocMarks[$pos])) {
2514					$firstMarkAssoc = $this->assocMarks[$pos];
2515				}
2516				// If all components of the ligature are marks, we call this a mark ligature.
2517				for ($i = 1; $i < count($GlyphPos); $i++) {
2518					// If subsequent components are not Marks as well - don't ligate
2519					//      $unistr = $this->OTLdata[$GlyphPos[$i]]['hex'];
2520					//      if (strpos($this->GlyphClassMarks, $unistr )===false) { return; }
2521
2522					$nextMarkAssoc = '';
2523					if (isset($this->assocMarks[$GlyphPos[$i]])) {
2524						$nextMarkAssoc = $this->assocMarks[$GlyphPos[$i]];
2525					}
2526					// If first component was attached to a previous ligature component,
2527					// all subsequent components should be attached to the same ligature
2528					// component, otherwise we shouldn't ligate them.
2529					// If first component was NOT attached to a previous ligature component,
2530					// all subsequent components should also NOT be attached to any ligature component,
2531					if ($firstMarkAssoc != $nextMarkAssoc) {
2532						// unless they are attached to the first component itself!
2533						//          if (!is_array($nextMarkAssoc) || $nextMarkAssoc['ligPos']!= $pos) { return; }
2534						// Update/Edit - In test with myanmartext font
2535						// &#x1004;&#x103a;&#x1039;&#x1000;&#x1039;&#x1000;&#x103b;&#x103c;&#x103d;&#x1031;&#x102d;
2536						// => Lookup 17  E003 E066B E05A 102D
2537						// E003 and 102D should form a mark ligature, but 102D is already associated with (non-mark) ligature E05A
2538						// So instead of disallowing the mark ligature to form, just dissociate...
2539						if (!is_array($nextMarkAssoc) || $nextMarkAssoc['ligPos'] != $pos) {
2540							unset($this->assocMarks[$GlyphPos[$i]]);
2541						}
2542					}
2543				}
2544
2545				/*
2546				 * - If it *is* a mark ligature, we don't allocate a new ligature id, and leave
2547				 *   the ligature to keep its old ligature id.  This will allow it to attach to
2548				 *   a base ligature in GPOS.  Eg. if the sequence is: LAM,LAM,SHADDA,FATHA,HEH,
2549				 *   and LAM,LAM,HEH form a ligature, they will leave SHADDA and FATHA wit a
2550				 *   ligature id and component value of 2.  Then if SHADDA,FATHA form a ligature
2551				 *   later, we don't want them to lose their ligature id/component, otherwise
2552				 *   GPOS will fail to correctly position the mark ligature on top of the
2553				 *   LAM,LAM,HEH ligature.
2554				 */
2555				// So if is_array($firstMarkAssoc) - the new (Mark) ligature should keep this association
2556
2557				$lastPos = $GlyphPos[(count($GlyphPos) - 1)];
2558			} else {
2559				/*
2560				 * - Ligatures cannot be formed across glyphs attached to different components
2561				 *   of previous ligatures.  Eg. the sequence is LAM,SHADDA,LAM,FATHA,HEH, and
2562				 *   LAM,LAM,HEH form a ligature, leaving SHADDA,FATHA next to eachother.
2563				 *   However, it would be wrong to ligate that SHADDA,FATHA sequence.
2564				 *   There is an exception to this: If a ligature tries ligating with marks that
2565				 *   belong to it itself, go ahead, assuming that the font designer knows what
2566				 *   they are doing (otherwise it can break Indic stuff when a matra wants to
2567				 *   ligate with a conjunct...)
2568				 */
2569
2570				/*
2571				 * - If a ligature is formed of components that some of which are also ligatures
2572				 *   themselves, and those ligature components had marks attached to *their*
2573				 *   components, we have to attach the marks to the new ligature component
2574				 *   positions!  Now *that*'s tricky!  And these marks may be following the
2575				 *   last component of the whole sequence, so we should loop forward looking
2576				 *   for them and update them.
2577				 *
2578				 *   Eg. the sequence is LAM,LAM,SHADDA,FATHA,HEH, and the font first forms a
2579				 *   'calt' ligature of LAM,HEH, leaving the SHADDA and FATHA with a ligature
2580				 *   id and component == 1.  Now, during 'liga', the LAM and the LAM-HEH ligature
2581				 *   form a LAM-LAM-HEH ligature.  We need to reassign the SHADDA and FATHA to
2582				 *   the new ligature with a component value of 2.
2583				 *
2584				 *   This in fact happened to a font...  See:
2585				 *   https://bugzilla.gnome.org/show_bug.cgi?id=437633
2586				 */
2587
2588				$currComp = 0;
2589				for ($i = 0; $i < count($GlyphPos); $i++) {
2590					if ($i > 0 && isset($this->assocLigs[$GlyphPos[$i]])) { // One of the other components is already a ligature
2591						$nc = $this->assocLigs[$GlyphPos[$i]];
2592					} else {
2593						$nc = 1;
2594					}
2595					// While next char to right is a mark (but not the next matched glyph)
2596					// ?? + also include a Mark Ligature here
2597					$ic = 1;
2598					while ((($i == count($GlyphPos) - 1) || (isset($GlyphPos[$i + 1]) && ($GlyphPos[$i] + $ic) < $GlyphPos[$i + 1])) && isset($this->OTLdata[($GlyphPos[$i] + $ic)]) && strpos($this->GlyphClassMarks, $this->OTLdata[($GlyphPos[$i] + $ic)]['hex']) !== false) {
2599						$newComp = $currComp;
2600						if (isset($this->assocMarks[$GlyphPos[$i] + $ic])) { // One of the inbetween Marks is already associated with a Lig
2601							// OK as long as it is associated with the current Lig
2602							//      if ($this->assocMarks[($GlyphPos[$i]+$ic)]['ligPos'] != ($GlyphPos[$i]+$ic)) { die("Problem #1"); }
2603							$newComp += $this->assocMarks[($GlyphPos[$i] + $ic)]['compID'];
2604						}
2605						$this->assocMarks[($GlyphPos[$i] + $ic)] = ['compID' => $newComp, 'ligPos' => $pos];
2606						$ic++;
2607					}
2608					$currComp += $nc;
2609				}
2610				$lastPos = $GlyphPos[(count($GlyphPos) - 1)] + $ic - 1;
2611				$this->assocLigs[$pos] = $currComp; // Number of components in new Ligature
2612			}
2613
2614			// Now remove the unwanted glyphs and associated metadata
2615			$newOTLdata[0] = [];
2616
2617			// Get types of new inserted chars - or replicate type of char being replaced
2618			//  $bt = Ucdn::get_bidi_class($substitute);
2619			//  if (!$bt) {
2620			$bt = $this->OTLdata[$pos]['bidi_type'];
2621			//  }
2622
2623			if (strpos($this->GlyphClassMarks, $this->unicode_hex($substitute)) !== false) {
2624				$gp = 'M';
2625			} elseif ($substitute == 32) {
2626				$gp = 'S';
2627			} else {
2628				$gp = 'C';
2629			}
2630
2631			// Need to update details of new glyphs inserted
2632			$newOTLdata[0]['general_category'] = $this->OTLdata[$pos]['general_category'];
2633
2634			$newOTLdata[0]['bidi_type'] = $bt;
2635			$newOTLdata[0]['group'] = $gp;
2636
2637			// KASHIDA: If forming a ligature when the last component was identified as a kashida point (final form)
2638			// If previous/first component of ligature is a medial form, then keep this as a kashida point
2639			// TEST (Arabic Typesetting) &#x64a;&#x64e;&#x646;&#x62a;&#x64f;&#x645;
2640			$ka = 0;
2641			if (isset($this->OTLdata[$GlyphPos[(count($GlyphPos) - 1)]]['GPOSinfo']['kashida'])) {
2642				$ka = $this->OTLdata[$GlyphPos[(count($GlyphPos) - 1)]]['GPOSinfo']['kashida'];
2643			}
2644			if ($ka == 1 && isset($this->OTLdata[$pos]['form']) && $this->OTLdata[$pos]['form'] == 3) {
2645				$newOTLdata[0]['GPOSinfo']['kashida'] = $ka;
2646			}
2647
2648			$newOTLdata[0]['uni'] = $substitute;
2649			$newOTLdata[0]['hex'] = $this->unicode_hex($substitute);
2650
2651			if ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
2652				$newOTLdata[0]['indic_category'] = $this->OTLdata[$pos]['indic_category'];
2653				$newOTLdata[0]['indic_position'] = $this->OTLdata[$pos]['indic_position'];
2654			} elseif ($this->shaper == 'M') {
2655				$newOTLdata[0]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category'];
2656				$newOTLdata[0]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position'];
2657			}
2658			if (isset($this->OTLdata[$pos]['mask'])) {
2659				$newOTLdata[0]['mask'] = $this->OTLdata[$pos]['mask'];
2660			}
2661			if (isset($this->OTLdata[$pos]['syllable'])) {
2662				$newOTLdata[0]['syllable'] = $this->OTLdata[$pos]['syllable'];
2663			}
2664
2665			$newOTLdata[0]['is_ligature'] = true;
2666
2667
2668			array_splice($this->OTLdata, $pos, 1, $newOTLdata);
2669
2670			// GlyphPos contains array of arr_pos to set null - not necessarily contiguous
2671			// +- Remove any assocMarks or assocLigs from the main components (the ones that are deleted)
2672			for ($i = count($GlyphPos) - 1; $i > 0; $i--) {
2673				$gpos = $GlyphPos[$i];
2674				array_splice($this->OTLdata, $gpos, 1);
2675				unset($this->assocLigs[$gpos]);
2676				unset($this->assocMarks[$gpos]);
2677			}
2678			//  $this->assocLigs = array(); // Ligatures[$posarr lpos] => nc
2679			//  $this->assocMarks = array();    // assocMarks[$posarr mpos] => array(compID, ligPos)
2680			// Update position of pre-existing Ligatures and associated Marks
2681			// Start after first GlyphPos
2682			// count($GlyphPos)-1  is the number of glyphs removed from string
2683			for ($p = ($GlyphPos[0] + 1); $p < (count($this->OTLdata) + count($GlyphPos) - 1); $p++) {
2684				$nrem = 0; // Number of Glyphs removed at this point in the string
2685				for ($i = 0; $i < count($GlyphPos); $i++) {
2686					if ($i > 0 && $p > $GlyphPos[$i]) {
2687						$nrem++;
2688					}
2689				}
2690				if (isset($this->assocLigs[$p])) {
2691					$tmp = $this->assocLigs[$p];
2692					unset($this->assocLigs[$p]);
2693					$this->assocLigs[($p - $nrem)] = $tmp;
2694				}
2695				if (isset($this->assocMarks[$p])) {
2696					$tmp = $this->assocMarks[$p];
2697					unset($this->assocMarks[$p]);
2698					if ($tmp['ligPos'] > $GlyphPos[0]) {
2699						$tmp['ligPos'] -= $nrem;
2700					}
2701					$this->assocMarks[($p - $nrem)] = $tmp;
2702				}
2703			}
2704			return 1;
2705		} else {
2706			return 0;
2707		}
2708	}
2709
2710	////////////////////////////////////////////////////////////////
2711	//////////       ARABIC        /////////////////////////////////
2712	////////////////////////////////////////////////////////////////
2713	private function arabic_initialise()
2714	{
2715		// cf. http://unicode.org/Public/UNIDATA/ArabicShaping.txt
2716		// http://unicode.org/Public/UNIDATA/extracted/DerivedJoiningType.txt
2717		// JOIN TO FOLLOWING LETTER IN LOGICAL ORDER (i.e. AS INITIAL/MEDIAL FORM) = Unicode Left-Joining (+ Dual-Joining + Join_Causing 00640)
2718		$this->arabLeftJoining = [
2719			0x0620 => 1, 0x0626 => 1, 0x0628 => 1, 0x062A => 1, 0x062B => 1, 0x062C => 1, 0x062D => 1, 0x062E => 1,
2720			0x0633 => 1, 0x0634 => 1, 0x0635 => 1, 0x0636 => 1, 0x0637 => 1, 0x0638 => 1, 0x0639 => 1, 0x063A => 1,
2721			0x063B => 1, 0x063C => 1, 0x063D => 1, 0x063E => 1, 0x063F => 1, 0x0640 => 1, 0x0641 => 1, 0x0642 => 1,
2722			0x0643 => 1, 0x0644 => 1, 0x0645 => 1, 0x0646 => 1, 0x0647 => 1, 0x0649 => 1, 0x064A => 1, 0x066E => 1,
2723			0x066F => 1, 0x0678 => 1, 0x0679 => 1, 0x067A => 1, 0x067B => 1, 0x067C => 1, 0x067D => 1, 0x067E => 1,
2724			0x067F => 1, 0x0680 => 1, 0x0681 => 1, 0x0682 => 1, 0x0683 => 1, 0x0684 => 1, 0x0685 => 1, 0x0686 => 1,
2725			0x0687 => 1, 0x069A => 1, 0x069B => 1, 0x069C => 1, 0x069D => 1, 0x069E => 1, 0x069F => 1, 0x06A0 => 1,
2726			0x06A1 => 1, 0x06A2 => 1, 0x06A3 => 1, 0x06A4 => 1, 0x06A5 => 1, 0x06A6 => 1, 0x06A7 => 1, 0x06A8 => 1,
2727			0x06A9 => 1, 0x06AA => 1, 0x06AB => 1, 0x06AC => 1, 0x06AD => 1, 0x06AE => 1, 0x06AF => 1, 0x06B0 => 1,
2728			0x06B1 => 1, 0x06B2 => 1, 0x06B3 => 1, 0x06B4 => 1, 0x06B5 => 1, 0x06B6 => 1, 0x06B7 => 1, 0x06B8 => 1,
2729			0x06B9 => 1, 0x06BA => 1, 0x06BB => 1, 0x06BC => 1, 0x06BD => 1, 0x06BE => 1, 0x06BF => 1, 0x06C1 => 1,
2730			0x06C2 => 1, 0x06CC => 1, 0x06CE => 1, 0x06D0 => 1, 0x06D1 => 1, 0x06FA => 1, 0x06FB => 1, 0x06FC => 1,
2731			0x06FF => 1,
2732			/* Arabic Supplement */
2733			0x0750 => 1, 0x0751 => 1, 0x0752 => 1, 0x0753 => 1, 0x0754 => 1, 0x0755 => 1, 0x0756 => 1, 0x0757 => 1,
2734			0x0758 => 1, 0x075C => 1, 0x075D => 1, 0x075E => 1, 0x075F => 1, 0x0760 => 1, 0x0761 => 1, 0x0762 => 1,
2735			0x0763 => 1, 0x0764 => 1, 0x0765 => 1, 0x0766 => 1, 0x0767 => 1, 0x0768 => 1, 0x0769 => 1, 0x076A => 1,
2736			0x076D => 1, 0x076E => 1, 0x076F => 1, 0x0770 => 1, 0x0772 => 1, 0x0775 => 1, 0x0776 => 1, 0x0777 => 1,
2737			0x077A => 1, 0x077B => 1, 0x077C => 1, 0x077D => 1, 0x077E => 1, 0x077F => 1,
2738			/* Extended Arabic */
2739			0x08A0 => 1, 0x08A2 => 1, 0x08A3 => 1, 0x08A4 => 1, 0x08A5 => 1, 0x08A6 => 1, 0x08A7 => 1, 0x08A8 => 1,
2740			0x08A9 => 1,
2741			/* 'syrc' Syriac */
2742			0x0712 => 1, 0x0713 => 1, 0x0714 => 1, 0x071A => 1, 0x071B => 1, 0x071C => 1, 0x071D => 1, 0x071F => 1,
2743			0x0720 => 1, 0x0721 => 1, 0x0722 => 1, 0x0723 => 1, 0x0724 => 1, 0x0725 => 1, 0x0726 => 1, 0x0727 => 1,
2744			0x0729 => 1, 0x072B => 1, 0x072D => 1, 0x072E => 1, 0x074E => 1, 0x074F => 1,
2745			/* N'Ko */
2746			0x07CA => 1, 0x07CB => 1, 0x07CC => 1, 0x07CD => 1, 0x07CE => 1, 0x07CF => 1, 0x07D0 => 1, 0x07D1 => 1,
2747			0x07D2 => 1, 0x07D3 => 1, 0x07D4 => 1, 0x07D5 => 1, 0x07D6 => 1, 0x07D7 => 1, 0x07D8 => 1, 0x07D9 => 1,
2748			0x07DA => 1, 0x07DB => 1, 0x07DC => 1, 0x07DD => 1, 0x07DE => 1, 0x07DF => 1, 0x07E0 => 1, 0x07E1 => 1,
2749			0x07E2 => 1, 0x07E3 => 1, 0x07E4 => 1, 0x07E5 => 1, 0x07E6 => 1, 0x07E7 => 1, 0x07E8 => 1, 0x07E9 => 1,
2750			0x07EA => 1, 0x07FA => 1,
2751			/* Mandaic */
2752			0x0841 => 1, 0x0842 => 1, 0x0843 => 1, 0x0844 => 1, 0x0845 => 1, 0x0847 => 1, 0x0848 => 1, 0x084A => 1,
2753			0x084B => 1, 0x084C => 1, 0x084D => 1, 0x084E => 1, 0x0850 => 1, 0x0851 => 1, 0x0852 => 1, 0x0853 => 1,
2754			0x0855 => 1,
2755			/* ZWJ U+200D */
2756			0x0200D => 1];
2757
2758		/* JOIN TO PREVIOUS LETTER IN LOGICAL ORDER (i.e. AS FINAL/MEDIAL FORM) = Unicode Right-Joining (+ Dual-Joining + Join_Causing) */
2759		$this->arabRightJoining = [
2760			0x0620 => 1, 0x0622 => 1, 0x0623 => 1, 0x0624 => 1, 0x0625 => 1, 0x0626 => 1, 0x0627 => 1, 0x0628 => 1,
2761			0x0629 => 1, 0x062A => 1, 0x062B => 1, 0x062C => 1, 0x062D => 1, 0x062E => 1, 0x062F => 1, 0x0630 => 1,
2762			0x0631 => 1, 0x0632 => 1, 0x0633 => 1, 0x0634 => 1, 0x0635 => 1, 0x0636 => 1, 0x0637 => 1, 0x0638 => 1,
2763			0x0639 => 1, 0x063A => 1, 0x063B => 1, 0x063C => 1, 0x063D => 1, 0x063E => 1, 0x063F => 1, 0x0640 => 1,
2764			0x0641 => 1, 0x0642 => 1, 0x0643 => 1, 0x0644 => 1, 0x0645 => 1, 0x0646 => 1, 0x0647 => 1, 0x0648 => 1,
2765			0x0649 => 1, 0x064A => 1, 0x066E => 1, 0x066F => 1, 0x0671 => 1, 0x0672 => 1, 0x0673 => 1, 0x0675 => 1,
2766			0x0676 => 1, 0x0677 => 1, 0x0678 => 1, 0x0679 => 1, 0x067A => 1, 0x067B => 1, 0x067C => 1, 0x067D => 1,
2767			0x067E => 1, 0x067F => 1, 0x0680 => 1, 0x0681 => 1, 0x0682 => 1, 0x0683 => 1, 0x0684 => 1, 0x0685 => 1,
2768			0x0686 => 1, 0x0687 => 1, 0x0688 => 1, 0x0689 => 1, 0x068A => 1, 0x068B => 1, 0x068C => 1, 0x068D => 1,
2769			0x068E => 1, 0x068F => 1, 0x0690 => 1, 0x0691 => 1, 0x0692 => 1, 0x0693 => 1, 0x0694 => 1, 0x0695 => 1,
2770			0x0696 => 1, 0x0697 => 1, 0x0698 => 1, 0x0699 => 1, 0x069A => 1, 0x069B => 1, 0x069C => 1, 0x069D => 1,
2771			0x069E => 1, 0x069F => 1, 0x06A0 => 1, 0x06A1 => 1, 0x06A2 => 1, 0x06A3 => 1, 0x06A4 => 1, 0x06A5 => 1,
2772			0x06A6 => 1, 0x06A7 => 1, 0x06A8 => 1, 0x06A9 => 1, 0x06AA => 1, 0x06AB => 1, 0x06AC => 1, 0x06AD => 1,
2773			0x06AE => 1, 0x06AF => 1, 0x06B0 => 1, 0x06B1 => 1, 0x06B2 => 1, 0x06B3 => 1, 0x06B4 => 1, 0x06B5 => 1,
2774			0x06B6 => 1, 0x06B7 => 1, 0x06B8 => 1, 0x06B9 => 1, 0x06BA => 1, 0x06BB => 1, 0x06BC => 1, 0x06BD => 1,
2775			0x06BE => 1, 0x06BF => 1, 0x06C0 => 1, 0x06C1 => 1, 0x06C2 => 1, 0x06C3 => 1, 0x06C4 => 1, 0x06C5 => 1,
2776			0x06C6 => 1, 0x06C7 => 1, 0x06C8 => 1, 0x06C9 => 1, 0x06CA => 1, 0x06CB => 1, 0x06CC => 1, 0x06CD => 1,
2777			0x06CE => 1, 0x06CF => 1, 0x06D0 => 1, 0x06D1 => 1, 0x06D2 => 1, 0x06D3 => 1, 0x06D5 => 1, 0x06EE => 1,
2778			0x06EF => 1, 0x06FA => 1, 0x06FB => 1, 0x06FC => 1, 0x06FF => 1,
2779			/* Arabic Supplement */
2780			0x0750 => 1, 0x0751 => 1, 0x0752 => 1, 0x0753 => 1, 0x0754 => 1, 0x0755 => 1, 0x0756 => 1, 0x0757 => 1,
2781			0x0758 => 1, 0x0759 => 1, 0x075A => 1, 0x075B => 1, 0x075C => 1, 0x075D => 1, 0x075E => 1, 0x075F => 1,
2782			0x0760 => 1, 0x0761 => 1, 0x0762 => 1, 0x0763 => 1, 0x0764 => 1, 0x0765 => 1, 0x0766 => 1, 0x0767 => 1,
2783			0x0768 => 1, 0x0769 => 1, 0x076A => 1, 0x076B => 1, 0x076C => 1, 0x076D => 1, 0x076E => 1, 0x076F => 1,
2784			0x0770 => 1, 0x0771 => 1, 0x0772 => 1, 0x0773 => 1, 0x0774 => 1, 0x0775 => 1, 0x0776 => 1, 0x0777 => 1,
2785			0x0778 => 1, 0x0779 => 1, 0x077A => 1, 0x077B => 1, 0x077C => 1, 0x077D => 1, 0x077E => 1, 0x077F => 1,
2786			/* Extended Arabic */
2787			0x08A0 => 1, 0x08A2 => 1, 0x08A3 => 1, 0x08A4 => 1, 0x08A5 => 1, 0x08A6 => 1, 0x08A7 => 1, 0x08A8 => 1,
2788			0x08A9 => 1, 0x08AA => 1, 0x08AB => 1, 0x08AC => 1,
2789			/* 'syrc' Syriac */
2790			0x0710 => 1, 0x0712 => 1, 0x0713 => 1, 0x0714 => 1, 0x0715 => 1, 0x0716 => 1, 0x0717 => 1, 0x0718 => 1,
2791			0x0719 => 1, 0x071A => 1, 0x071B => 1, 0x071C => 1, 0x071D => 1, 0x071E => 1, 0x071F => 1, 0x0720 => 1,
2792			0x0721 => 1, 0x0722 => 1, 0x0723 => 1, 0x0724 => 1, 0x0725 => 1, 0x0726 => 1, 0x0727 => 1, 0x0728 => 1,
2793			0x0729 => 1, 0x072A => 1, 0x072B => 1, 0x072C => 1, 0x072D => 1, 0x072E => 1, 0x072F => 1, 0x074D => 1,
2794			0x074E => 1, 0x074F,
2795			/* N'Ko */
2796			0x07CA => 1, 0x07CB => 1, 0x07CC => 1, 0x07CD => 1, 0x07CE => 1, 0x07CF => 1, 0x07D0 => 1, 0x07D1 => 1,
2797			0x07D2 => 1, 0x07D3 => 1, 0x07D4 => 1, 0x07D5 => 1, 0x07D6 => 1, 0x07D7 => 1, 0x07D8 => 1, 0x07D9 => 1,
2798			0x07DA => 1, 0x07DB => 1, 0x07DC => 1, 0x07DD => 1, 0x07DE => 1, 0x07DF => 1, 0x07E0 => 1, 0x07E1 => 1,
2799			0x07E2 => 1, 0x07E3 => 1, 0x07E4 => 1, 0x07E5 => 1, 0x07E6 => 1, 0x07E7 => 1, 0x07E8 => 1, 0x07E9 => 1,
2800			0x07EA => 1, 0x07FA => 1,
2801			/* Mandaic */
2802			0x0841 => 1, 0x0842 => 1, 0x0843 => 1, 0x0844 => 1, 0x0845 => 1, 0x0847 => 1, 0x0848 => 1, 0x084A => 1,
2803			0x084B => 1, 0x084C => 1, 0x084D => 1, 0x084E => 1, 0x0850 => 1, 0x0851 => 1, 0x0852 => 1, 0x0853 => 1,
2804			0x0855 => 1,
2805			0x0840 => 1, 0x0846 => 1, 0x0849 => 1, 0x084F => 1, 0x0854 => 1, /* Right joining */
2806			/* ZWJ U+200D */
2807			0x0200D => 1];
2808
2809		/* VOWELS = TRANSPARENT-JOINING = Unicode Transparent-Joining type (not just vowels) */
2810		$this->arabTransparent = [
2811			0x0610 => 1, 0x0611 => 1, 0x0612 => 1, 0x0613 => 1, 0x0614 => 1, 0x0615 => 1, 0x0616 => 1, 0x0617 => 1,
2812			0x0618 => 1, 0x0619 => 1, 0x061A => 1, 0x064B => 1, 0x064C => 1, 0x064D => 1, 0x064E => 1, 0x064F => 1,
2813			0x0650 => 1, 0x0651 => 1, 0x0652 => 1, 0x0653 => 1, 0x0654 => 1, 0x0655 => 1, 0x0656 => 1, 0x0657 => 1,
2814			0x0658 => 1, 0x0659 => 1, 0x065A => 1, 0x065B => 1, 0x065C => 1, 0x065D => 1, 0x065E => 1, 0x065F => 1,
2815			0x0670 => 1, 0x06D6 => 1, 0x06D7 => 1, 0x06D8 => 1, 0x06D9 => 1, 0x06DA => 1, 0x06DB => 1, 0x06DC => 1,
2816			0x06DF => 1, 0x06E0 => 1, 0x06E1 => 1, 0x06E2 => 1, 0x06E3 => 1, 0x06E4 => 1, 0x06E7 => 1, 0x06E8 => 1,
2817			0x06EA => 1, 0x06EB => 1, 0x06EC => 1, 0x06ED => 1,
2818			/* Extended Arabic */
2819			0x08E4 => 1, 0x08E5 => 1, 0x08E6 => 1, 0x08E7 => 1, 0x08E8 => 1, 0x08E9 => 1, 0x08EA => 1, 0x08EB => 1,
2820			0x08EC => 1, 0x08ED => 1, 0x08EE => 1, 0x08EF => 1, 0x08F0 => 1, 0x08F1 => 1, 0x08F2 => 1, 0x08F3 => 1,
2821			0x08F4 => 1, 0x08F5 => 1, 0x08F6 => 1, 0x08F7 => 1, 0x08F8 => 1, 0x08F9 => 1, 0x08FA => 1, 0x08FB => 1,
2822			0x08FC => 1, 0x08FD => 1, 0x08FE => 1,
2823			/* Arabic ligatures in presentation form (converted in 'ccmp' in e.g. Arial and Times ? need to add others in this range) */
2824			0xFC5E => 1, 0xFC5F => 1, 0xFC60 => 1, 0xFC61 => 1, 0xFC62 => 1,
2825			/*  'syrc' Syriac */
2826			0x070F => 1, 0x0711 => 1, 0x0730 => 1, 0x0731 => 1, 0x0732 => 1, 0x0733 => 1, 0x0734 => 1, 0x0735 => 1,
2827			0x0736 => 1, 0x0737 => 1, 0x0738 => 1, 0x0739 => 1, 0x073A => 1, 0x073B => 1, 0x073C => 1, 0x073D => 1,
2828			0x073E => 1, 0x073F => 1, 0x0740 => 1, 0x0741 => 1, 0x0742 => 1, 0x0743 => 1, 0x0744 => 1, 0x0745 => 1,
2829			0x0746 => 1, 0x0747 => 1, 0x0748 => 1, 0x0749 => 1, 0x074A => 1,
2830			/* N'Ko */
2831			0x07EB => 1, 0x07EC => 1, 0x07ED => 1, 0x07EE => 1, 0x07EF => 1, 0x07F0 => 1, 0x07F1 => 1, 0x07F2 => 1,
2832			0x07F3 => 1,
2833			/* Mandaic */
2834			0x0859 => 1, 0x085A => 1, 0x085B => 1,
2835		];
2836	}
2837
2838	private function arabic_shaper($usetags, $scriptTag)
2839	{
2840		$chars = [];
2841		for ($i = 0; $i < count($this->OTLdata); $i++) {
2842			$chars[] = $this->OTLdata[$i]['hex'];
2843		}
2844
2845		$crntChar = null;
2846		$prevChar = null;
2847		$nextChar = null;
2848		$output = [];
2849		$max = count($chars);
2850		for ($i = $max - 1; $i >= 0; $i--) {
2851			$crntChar = $chars[$i];
2852			if ($i > 0) {
2853				$prevChar = hexdec($chars[$i - 1]);
2854			} else {
2855				$prevChar = null;
2856			}
2857			if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 2])) {
2858				$prevChar = hexdec($chars[$i - 2]);
2859				if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 3])) {
2860					$prevChar = hexdec($chars[$i - 3]);
2861					if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 4])) {
2862						$prevChar = hexdec($chars[$i - 4]);
2863					}
2864				}
2865			}
2866			if ($crntChar && isset($this->arabTransparentJoin[hexdec($crntChar)])) {
2867				// If next_char = RightJoining && prev_char = LeftJoining:
2868				if (isset($chars[$i + 1]) && $chars[$i + 1] && isset($this->arabRightJoining[hexdec($chars[$i + 1])]) && $prevChar && isset($this->arabLeftJoining[$prevChar])) {
2869					$output[] = $this->get_arab_glyphs($crntChar, 1, $chars, $i, $scriptTag, $usetags); // <final> form
2870				} else {
2871					$output[] = $this->get_arab_glyphs($crntChar, 0, $chars, $i, $scriptTag, $usetags);  // <isolated> form
2872				}
2873				continue;
2874			}
2875			if (hexdec($crntChar) < 128) {
2876				$output[] = [$crntChar, 0];
2877				$nextChar = $crntChar;
2878				continue;
2879			}
2880			// 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL
2881			$form = 0;
2882			if ($prevChar && isset($this->arabLeftJoining[$prevChar])) {
2883				$form++;
2884			}
2885			if ($nextChar && isset($this->arabRightJoining[hexdec($nextChar)])) {
2886				$form += 2;
2887			}
2888			$output[] = $this->get_arab_glyphs($crntChar, $form, $chars, $i, $scriptTag, $usetags);
2889			$nextChar = $crntChar;
2890		}
2891		$ra = array_reverse($output);
2892		for ($i = 0; $i < count($this->OTLdata); $i++) {
2893			$this->OTLdata[$i]['uni'] = hexdec($ra[$i][0]);
2894			$this->OTLdata[$i]['hex'] = $ra[$i][0];
2895			$this->OTLdata[$i]['form'] = $ra[$i][1]; // Actaul form substituted 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL
2896		}
2897	}
2898
2899	private function get_arab_glyphs($char, $type, &$chars, $i, $scriptTag, $usetags)
2900	{
2901		// Optional Feature settings    // doesn't control Syriac at present
2902		if (($type === 0 && strpos($usetags, 'isol') === false) || ($type === 1 && strpos($usetags, 'fina') === false) || ($type === 2 && strpos($usetags, 'init') === false) || ($type === 3 && strpos($usetags, 'medi') === false)) {
2903			return [$char, 0];
2904		}
2905
2906		// 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL (:: 4=MED2 :: 5=FIN2 :: 6=FIN3)
2907		$retk = -1;
2908		// Alaph 00710 in Syriac
2909		if ($scriptTag == 'syrc' && $char == '00710') {
2910			// if there is a preceding (base?) character *** should search back to previous base - ignoring vowels and change $n
2911			// set $n as the position of the last base; for now we'll just do this:
2912			$n = $i - 1;
2913			// if the preceding (base) character cannot be joined to
2914			// not in $this->arabLeftJoining i.e. not a char which can join to the next one
2915			if (isset($chars[$n]) && isset($this->arabLeftJoining[hexdec($chars[$n])])) {
2916				// if in the middle of Syriac words
2917				if (isset($chars[$i + 1]) && preg_match('/[\x{0700}-\x{0745}]/u', UtfString::code2utf(hexdec($chars[$n]))) && preg_match('/[\x{0700}-\x{0745}]/u', UtfString::code2utf(hexdec($chars[$i + 1]))) && isset($this->arabGlyphs[$char][4])) {
2918					$retk = 4;
2919				} // if at the end of Syriac words
2920				elseif (!isset($chars[$i + 1]) || !preg_match('/[\x{0700}-\x{0745}]/u', UtfString::code2utf(hexdec($chars[$i + 1])))) {
2921					// if preceding base character IS (00715|00716|0072A)
2922					if (strpos('0715|0716|072A', $chars[$n]) !== false && isset($this->arabGlyphs[$char][6])) {
2923						$retk = 6;
2924					} // elseif preceding base character is NOT (00715|00716|0072A)
2925					elseif (isset($this->arabGlyphs[$char][5])) {
2926						$retk = 5;
2927					}
2928				}
2929			}
2930			if ($retk != -1) {
2931				return [$this->arabGlyphs[$char][$retk], $retk];
2932			} else {
2933				return [$char, 0];
2934			}
2935		}
2936
2937		if (($type > 0 || $type === 0) && isset($this->arabGlyphs[$char][$type])) {
2938			$retk = $type;
2939		} elseif ($type == 3 && isset($this->arabGlyphs[$char][1])) { // if <medial> not defined, but <final>, return <final>
2940			$retk = 1;
2941		} elseif ($type == 2 && isset($this->arabGlyphs[$char][0])) { // if <initial> not defined, but <isolated>, return <isolated>
2942			$retk = 0;
2943		}
2944		if ($retk != -1) {
2945			$match = true;
2946			// If GSUB includes a Backtrack or Lookahead condition (e.g. font ArabicTypesetting)
2947			if (isset($this->arabGlyphs[$char]['prel'][$retk]) && $this->arabGlyphs[$char]['prel'][$retk]) {
2948				$ig = 1;
2949				foreach ($this->arabGlyphs[$char]['prel'][$retk] as $k => $v) { // $k starts 0, 1...
2950					if (!isset($chars[$i - $ig - $k])) {
2951						$match = false;
2952					} elseif (strpos($v, $chars[$i - $ig - $k]) === false) {
2953						while (strpos($this->arabGlyphs[$char]['ignore'][$retk], $chars[$i - $ig - $k]) !== false) {  // ignore
2954							$ig++;
2955						}
2956						if (!isset($chars[$i - $ig - $k])) {
2957							$match = false;
2958						} elseif (strpos($v, $chars[$i - $ig - $k]) === false) {
2959							$match = false;
2960						}
2961					}
2962				}
2963			}
2964			if (isset($this->arabGlyphs[$char]['postl'][$retk]) && $this->arabGlyphs[$char]['postl'][$retk]) {
2965				$ig = 1;
2966				foreach ($this->arabGlyphs[$char]['postl'][$retk] as $k => $v) { // $k starts 0, 1...
2967					if (!isset($chars[$i + $ig + $k])) {
2968						$match = false;
2969					} elseif (strpos($v, $chars[$i + $ig + $k]) === false) {
2970						while (strpos($this->arabGlyphs[$char]['ignore'][$retk], $chars[$i + $ig + $k]) !== false) {  // ignore
2971							$ig++;
2972						}
2973						if (!isset($chars[$i + $ig + $k])) {
2974							$match = false;
2975						} elseif (strpos($v, $chars[$i + $ig + $k]) === false) {
2976							$match = false;
2977						}
2978					}
2979				}
2980			}
2981			if ($match) {
2982				return [$this->arabGlyphs[$char][$retk], $retk];
2983			} else {
2984				return [$char, 0];
2985			}
2986		} else {
2987			return [$char, 0];
2988		}
2989	}
2990
2991	////////////////////////////////////////////////////////////////
2992	/////////////////       LINE BREAKING    ///////////////////////
2993	////////////////////////////////////////////////////////////////
2994	/////////////       TIBETAN LINE BREAKING    ///////////////////
2995	////////////////////////////////////////////////////////////////
2996	// Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
2997	private function tibetanLineBreaking()
2998	{
2999		for ($ptr = 0; $ptr < count($this->OTLdata); $ptr++) {
3000			// Break opportunities at U+0F0B Tsheg or U=0F0D
3001			if (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] == 0x0F0B || $this->OTLdata[$ptr]['uni'] == 0x0F0D)) {
3002				if (isset($this->OTLdata[$ptr + 1]['uni']) && ($this->OTLdata[$ptr + 1]['uni'] == 0x0F0D || $this->OTLdata[$ptr + 1]['uni'] == 0xF0E)) {
3003					continue;
3004				}
3005				// Set end of word marker in OTLdata at matchpos
3006				$this->OTLdata[$ptr]['wordend'] = true;
3007			}
3008		}
3009	}
3010
3011	/**
3012	 * South East Asian Linebreaking (Thai, Khmer and Lao) using dictionary of words
3013	 *
3014	 * Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
3015	 */
3016	private function seaLineBreaking()
3017	{
3018		// Load Line-breaking dictionary
3019		if (!isset($this->lbdicts[$this->shaper]) && file_exists(__DIR__ . '/../data/linebrdict' . $this->shaper . '.dat')) {
3020			$this->lbdicts[$this->shaper] = file_get_contents(__DIR__ . '/../data/linebrdict' . $this->shaper . '.dat');
3021		}
3022
3023		$dict = &$this->lbdicts[$this->shaper];
3024
3025		// Find all word boundaries and mark end of word $this->OTLdata[$i]['wordend']=true on last character
3026		// If Thai, allow for possible suffixes (not in Lao or Khmer)
3027		// repeater/ellision characters
3028		// (0x0E2F);        // Ellision character THAI_PAIYANNOI 0x0E2F  UTF-8 0xE0 0xB8 0xAF
3029		// (0x0E46);        // Repeat character THAI_MAIYAMOK 0x0E46   UTF-8 0xE0 0xB9 0x86
3030		// (0x0EC6);        // Repeat character LAO   UTF-8 0xE0 0xBB 0x86
3031
3032		$rollover = [];
3033		$ptr = 0;
3034
3035		while ($ptr < count($this->OTLdata) - 3) {
3036			if (count($rollover)) {
3037				$matches = $rollover;
3038				$rollover = [];
3039			} else {
3040				$matches = $this->checkwordmatch($dict, $ptr);
3041			}
3042			if (count($matches) == 1) {
3043				$matchpos = $matches[0];
3044				// Check for repeaters - if so $matchpos++
3045				if (isset($this->OTLdata[$matchpos + 1]['uni']) && ($this->OTLdata[$matchpos + 1]['uni'] == 0x0E2F || $this->OTLdata[$matchpos + 1]['uni'] == 0x0E46 || $this->OTLdata[$matchpos + 1]['uni'] == 0x0EC6)) {
3046					$matchpos++;
3047				}
3048				// Set end of word marker in OTLdata at matchpos
3049				$this->OTLdata[$matchpos]['wordend'] = true;
3050				$ptr = $matchpos + 1;
3051			} elseif (empty($matches)) {
3052				$ptr++;
3053				// Move past any ASCII characters
3054				while (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] >> 8) == 0) {
3055					$ptr++;
3056				}
3057			} else { // Multiple matches
3058				$secondmatch = false;
3059				for ($m = count($matches) - 1; $m >= 0; $m--) {
3060					//for ($m=0;$m<count($matches);$m++) {
3061					$firstmatch = $matches[$m];
3062					$matches2 = $this->checkwordmatch($dict, $firstmatch + 1);
3063					if (count($matches2)) {
3064						// Set end of word marker in OTLdata at matchpos
3065						$this->OTLdata[$firstmatch]['wordend'] = true;
3066						$ptr = $firstmatch + 1;
3067						$rollover = $matches2;
3068						$secondmatch = true;
3069						break;
3070					}
3071				}
3072				if (!$secondmatch) {
3073					// Set end of word marker in OTLdata at end of longest first match
3074					$this->OTLdata[$matches[count($matches) - 1]]['wordend'] = true;
3075					$ptr = $matches[count($matches) - 1] + 1;
3076					// Move past any ASCII characters
3077					while (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] >> 8) == 0) {
3078						$ptr++;
3079					}
3080				}
3081			}
3082		}
3083	}
3084
3085	private function checkwordmatch(&$dict, $ptr)
3086	{
3087		/*
3088		  Node type: Split.
3089		  Divide at < 98 >= 98
3090		  Offset for >= 98 == 79    (long 4-byte unsigned)
3091
3092		  Node type: Linear match.
3093		  Char = 97
3094
3095		  Intermediate match
3096
3097		  Final match
3098		 */
3099
3100		$dictptr = 0;
3101		$ok = true;
3102		$matches = [];
3103		while ($ok) {
3104			$x = ord($dict[$dictptr]);
3105			$c = $this->OTLdata[$ptr]['uni'] & 0xFF;
3106			if ($x == static::_DICT_INTERMEDIATE_MATCH) {
3107//echo "DICT_INTERMEDIATE_MATCH: ".dechex($c).'<br />';
3108				// Do not match if next character in text is a Mark
3109				if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3110					$matches[] = $ptr - 1;
3111				}
3112				$dictptr++;
3113			} elseif ($x == static::_DICT_FINAL_MATCH) {
3114//echo "DICT_FINAL_MATCH: ".dechex($c).'<br />';
3115				// Do not match if next character in text is a Mark
3116				if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3117					$matches[] = $ptr - 1;
3118				}
3119				return $matches;
3120			} elseif ($x == static::_DICT_NODE_TYPE_LINEAR) {
3121//echo "DICT_NODE_TYPE_LINEAR: ".dechex($c).'<br />';
3122				$dictptr++;
3123				$m = ord($dict[$dictptr]);
3124				if ($c == $m) {
3125					$ptr++;
3126					if ($ptr > count($this->OTLdata) - 1) {
3127						$next = ord($dict[$dictptr + 1]);
3128						if ($next == static::_DICT_INTERMEDIATE_MATCH || $next == static::_DICT_FINAL_MATCH) {
3129							// Do not match if next character in text is a Mark
3130							if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3131								$matches[] = $ptr - 1;
3132							}
3133						}
3134						return $matches;
3135					}
3136					$dictptr++;
3137					continue;
3138				} else {
3139//echo "DICT_NODE_TYPE_LINEAR NOT: ".dechex($c).'<br />';
3140					return $matches;
3141				}
3142			} elseif ($x == static::_DICT_NODE_TYPE_SPLIT) {
3143//echo "DICT_NODE_TYPE_SPLIT ON ".dechex($d).": ".dechex($c).'<br />';
3144				$dictptr++;
3145				$d = ord($dict[$dictptr]);
3146				if ($c < $d) {
3147					$dictptr += 5;
3148				} else {
3149					$dictptr++;
3150					// Unsigned long 32-bit offset
3151					$offset = (ord($dict[$dictptr]) * 16777216) + (ord($dict[$dictptr + 1]) << 16) + (ord($dict[$dictptr + 2]) << 8) + ord($dict[$dictptr + 3]);
3152					$dictptr = $offset;
3153				}
3154			} else {
3155//echo "PROBLEM: ".($x).'<br />';
3156				$ok = false; // Something has gone wrong
3157			}
3158		}
3159
3160		return $matches;
3161	}
3162
3163	////////////////////////////////////////////////////////////////
3164	//////////       GPOS    ///////////////////////////////////////
3165	////////////////////////////////////////////////////////////////
3166	private function _applyGPOSrules($LookupList, $is_old_spec = false)
3167	{
3168		foreach ($LookupList as $lu => $tag) {
3169			$Type = $this->GPOSLookups[$lu]['Type'];
3170			$Flag = $this->GPOSLookups[$lu]['Flag'];
3171			$MarkFilteringSet = '';
3172			if (isset($this->GPOSLookups[$lu]['MarkFilteringSet'])) {
3173				$MarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
3174			}
3175			$ptr = 0;
3176			// Test each glyph sequentially
3177			while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
3178				$currGlyph = $this->OTLdata[$ptr]['hex'];
3179				$currGID = $this->OTLdata[$ptr]['uni'];
3180				$shift = 1;
3181				foreach ($this->GPOSLookups[$lu]['Subtables'] as $c => $subtable_offset) {
3182					// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
3183					if (isset($this->LuCoverage[$lu][$c][$currGID])) {
3184						// Get rules from font GPOS subtable
3185						if (isset($this->OTLdata[$ptr]['bidi_type'])) {  // No need to check bidi_type - just a check that it exists
3186							$shift = $this->_applyGPOSsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GPOS_offset + $this->GSUB_length), $Type, $Flag, $MarkFilteringSet, $this->LuCoverage[$lu][$c], $tag, 0, $is_old_spec);
3187							if ($shift) {
3188								break;
3189							}
3190						}
3191					}
3192				}
3193				if ($shift == 0) {
3194					$shift = 1;
3195				}
3196				$ptr += $shift;
3197			}
3198		}
3199	}
3200
3201	//////////////////////////////////////////////////////////////////////////////////
3202	// GPOS Types
3203	// Lookup Type 1: Single Adjustment Positioning Subtable        Adjust position of a single glyph
3204	// Lookup Type 2: Pair Adjustment Positioning Subtable      Adjust position of a pair of glyphs
3205	// Lookup Type 3: Cursive Attachment Positioning Subtable       Attach cursive glyphs
3206	// Lookup Type 4: MarkToBase Attachment Positioning Subtable    Attach a combining mark to a base glyph
3207	// Lookup Type 5: MarkToLigature Attachment Positioning Subtable    Attach a combining mark to a ligature
3208	// Lookup Type 6: MarkToMark Attachment Positioning Subtable    Attach a combining mark to another mark
3209	// Lookup Type 7: Contextual Positioning Subtables          Position one or more glyphs in context
3210	// Lookup Type 8: Chaining Contextual Positioning Subtable      Position one or more glyphs in chained context
3211	// Lookup Type 9: Extension positioning
3212	//////////////////////////////////////////////////////////////////////////////////
3213	private function _applyGPOSvaluerecord($basepos, $Value)
3214	{
3215
3216		// If current glyph is a mark with a defined width, any XAdvance is considered to REPLACE the character Advance Width
3217		// Test case <div style="font-family:myanmartext">&#x1004;&#x103a;&#x1039;&#x1000;&#x1039;&#x1000;&#x103b;&#x103c;&#x103d;&#x1031;&#x102d;</div>
3218		if (strpos($this->GlyphClassMarks, $this->OTLdata[$basepos]['hex']) !== false) {
3219			$cw = round($this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$basepos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000); // convert back to font design units
3220		} else {
3221			$cw = 0;
3222		}
3223
3224		$apos = $this->_getXAdvancePos($basepos);
3225
3226		if (isset($Value['XAdvance']) && ($Value['XAdvance'] - $cw) != 0) {
3227			// However DON'T REPLACE the character Advance Width if Advance Width is negative
3228			// Test case <div style="font-family: dejavusansmono">&#x440;&#x443;&#x301;&#x441;&#x441;&#x43a;&#x438;&#x439;</div>
3229			if ($Value['XAdvance'] < 0) {
3230				$cw = 0;
3231			}
3232
3233			// For LTR apply XAdvanceL to the last mark following the base = at $apos
3234			// For RTL apply XAdvanceR to base = at $basepos
3235			if (isset($this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'])) {
3236				$this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'] += $Value['XAdvance'] - $cw;
3237			} else {
3238				$this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'] = $Value['XAdvance'] - $cw;
3239			}
3240			if (isset($this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'])) {
3241				$this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'] += $Value['XAdvance'] - $cw;
3242			} else {
3243				$this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'] = $Value['XAdvance'] - $cw;
3244			}
3245		}
3246
3247		// Any XPlacement (? and Y Placement) apply to base and marks (from basepos to apos)
3248		for ($a = $basepos; $a <= $apos; $a++) {
3249			if (isset($Value['XPlacement'])) {
3250				if (isset($this->OTLdata[$a]['GPOSinfo']['XPlacement'])) {
3251					$this->OTLdata[$a]['GPOSinfo']['XPlacement'] += $Value['XPlacement'];
3252				} else {
3253					$this->OTLdata[$a]['GPOSinfo']['XPlacement'] = $Value['XPlacement'];
3254				}
3255			}
3256			if (isset($Value['YPlacement'])) {
3257				if (isset($this->OTLdata[$a]['GPOSinfo']['YPlacement'])) {
3258					$this->OTLdata[$a]['GPOSinfo']['YPlacement'] += $Value['YPlacement'];
3259				} else {
3260					$this->OTLdata[$a]['GPOSinfo']['YPlacement'] = $Value['YPlacement'];
3261				}
3262			}
3263		}
3264	}
3265
3266	// If XAdvance is aplied to $ptr - in order for PDF to position the Advance correctly need to place it on
3267	// the last of any Marks which immediately follow the current glyph
3268	private function _getXAdvancePos($pos)
3269	{
3270		// NB Not all fonts have all marks specified in GlyphClassMarks
3271		// If the current glyph is not a base (but a mark) then ignore this, and apply to the current position
3272		if (strpos($this->GlyphClassMarks, $this->OTLdata[$pos]['hex']) !== false) {
3273			return $pos;
3274		}
3275
3276		while (isset($this->OTLdata[$pos + 1]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$pos + 1]['hex']) !== false) {
3277			$pos++;
3278		}
3279		return $pos;
3280	}
3281
3282	private function _applyGPOSsubtable($lookupID, $subtable, $ptr, $currGlyph, $currGID, $subtable_offset, $Type, $Flag, $MarkFilteringSet, $LuCoverage, $tag, $level, $is_old_spec)
3283	{
3284		if (($Flag & 0x0001) == 1) {
3285			$dir = 'RTL';
3286		} else { // only used for Type 3
3287			$dir = 'LTR';
3288		}
3289
3290		$ignore = $this->_getGCOMignoreString($Flag, $MarkFilteringSet);
3291
3292		// Lets start
3293		$this->seek($subtable_offset);
3294		$PosFormat = $this->read_ushort();
3295
3296		////////////////////////////////////////////////////////////////////////////////
3297		// LookupType 1: Single adjustment  Adjust position of a single glyph (e.g. SmallCaps/Sups/Subs)
3298		////////////////////////////////////////////////////////////////////////////////
3299		if ($Type == 1) {
3300			//===========
3301			// Format 1:
3302			//===========
3303			if ($PosFormat == 1) {
3304				$Coverage = $subtable_offset + $this->read_ushort();
3305				$ValueFormat = $this->read_ushort();
3306				$Value = $this->_getValueRecord($ValueFormat);
3307			} //===========
3308			// Format 2:
3309			//===========
3310			elseif ($PosFormat == 2) {
3311				$Coverage = $