1 <?php
2 
3 namespace Mpdf;
4 
5 use Mpdf\Strict;
6 
7 use Mpdf\Css\TextVars;
8 use Mpdf\Fonts\FontCache;
9 
10 use Mpdf\Shaper\Indic;
11 use Mpdf\Shaper\Myanmar;
12 use Mpdf\Shaper\Sea;
13 
14 use Mpdf\Utils\UtfString;
15 
16 class 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 = $subtable_offset + $this->read_ushort();
3312 				$ValueFormat = $this->read_ushort();
3313 				$ValueCount = $this->read_ushort();
3314 				$GlyphPos = $LuCoverage[$currGID];
3315 				$this->skip($GlyphPos * 2 * $this->count_bits($ValueFormat));
3316 				$Value = $this->_getValueRecord($ValueFormat);
3317 			}
3318 			$this->_applyGPOSvaluerecord($ptr, $Value);
3319 			if ($this->debugOTL) {
3320 				$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3321 			}
3322 			return 1;
3323 		} ////////////////////////////////////////////////////////////////////////////////
3324 		// LookupType 2: Pair adjustment    Adjust position of a pair of glyphs (Kerning)
3325 		////////////////////////////////////////////////////////////////////////////////
3326 		elseif ($Type == 2) {
3327 			$Coverage = $subtable_offset + $this->read_ushort();
3328 			$ValueFormat1 = $this->read_ushort();
3329 			$ValueFormat2 = $this->read_ushort();
3330 			$sizeOfPair = ( 2 * $this->count_bits($ValueFormat1) ) + ( 2 * $this->count_bits($ValueFormat2) );
3331 			//===========
3332 			// Format 1:
3333 			//===========
3334 			if ($PosFormat == 1) {
3335 				$PairSetCount = $this->read_ushort();
3336 				$PairSetOffset = [];
3337 				for ($p = 0; $p < $PairSetCount; $p++) {
3338 					$PairSetOffset[] = $subtable_offset + $this->read_ushort();
3339 				}
3340 				for ($p = 0; $p < $PairSetCount; $p++) {
3341 					if (isset($LuCoverage[$currGID]) && $LuCoverage[$currGID] == $p) {
3342 						$this->seek($PairSetOffset[$p]);
3343 						//PairSet table
3344 						$PairValueCount = $this->read_ushort();
3345 						for ($pv = 0; $pv < $PairValueCount; $pv++) {
3346 							//PairValueRecord
3347 							$gid = $this->read_ushort();
3348 							$SecondGlyph = $this->glyphToChar($gid);
3349 							$FirstGlyph = $this->OTLdata[$ptr]['uni'];
3350 
3351 							$checkpos = $ptr;
3352 							$checkpos++;
3353 							while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
3354 								$checkpos++;
3355 							}
3356 							if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == $SecondGlyph) {
3357 								$matchedpos = $checkpos;
3358 							} else {
3359 								$matchedpos = false;
3360 							}
3361 
3362 							if ($matchedpos !== false) {
3363 								$Value1 = $this->_getValueRecord($ValueFormat1);
3364 								$Value2 = $this->_getValueRecord($ValueFormat2);
3365 								if ($ValueFormat1) {
3366 									$this->_applyGPOSvaluerecord($ptr, $Value1);
3367 								}
3368 								if ($ValueFormat2) {
3369 									$this->_applyGPOSvaluerecord($matchedpos, $Value2);
3370 									if ($this->debugOTL) {
3371 										$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3372 									}
3373 									return $matchedpos - $ptr + 1;
3374 								}
3375 								if ($this->debugOTL) {
3376 									$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3377 								}
3378 								return $matchedpos - $ptr;
3379 							} else {
3380 								$this->skip($sizeOfPair);
3381 							}
3382 						}
3383 					}
3384 				}
3385 				return 0;
3386 			} //===========
3387 			// Format 2:
3388 			//===========
3389 			elseif ($PosFormat == 2) {
3390 				$ClassDef1 = $subtable_offset + $this->read_ushort();
3391 				$ClassDef2 = $subtable_offset + $this->read_ushort();
3392 				$Class1Count = $this->read_ushort();
3393 				$Class2Count = $this->read_ushort();
3394 
3395 				$sizeOfValueRecords = $Class1Count * $Class2Count * $sizeOfPair;
3396 
3397 				//$this->skip($sizeOfValueRecords );  ???? NOT NEEDED
3398 				// NB Class1Count includes Class 0 even though it is not defined by $ClassDef1
3399 				// i.e. Class1Count = 5; Class1 will contain array(indices 1-4);
3400 				$Class1 = $this->_getClassDefinitionTable($ClassDef1);
3401 				$Class2 = $this->_getClassDefinitionTable($ClassDef2);
3402 				$FirstGlyph = $this->OTLdata[$ptr]['uni'];
3403 				$checkpos = $ptr;
3404 				$checkpos++;
3405 				while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
3406 					$checkpos++;
3407 				}
3408 				if (isset($this->OTLdata[$checkpos])) {
3409 					$matchedpos = $checkpos;
3410 				} else {
3411 					return 0;
3412 				}
3413 
3414 				$SecondGlyph = $this->OTLdata[$matchedpos]['uni'];
3415 				for ($i = 0; $i < $Class1Count; $i++) {
3416 					if (isset($Class1[$i]) && count($Class1[$i])) {
3417 						$FirstClassPos = array_search($FirstGlyph, $Class1[$i]);
3418 						if ($FirstClassPos === false) {
3419 							continue;
3420 						} else {
3421 							for ($j = 0; $j < $Class2Count; $j++) {
3422 								if (isset($Class2[$j]) && count($Class2[$j])) {
3423 									$SecondClassPos = array_search($SecondGlyph, $Class2[$j]);
3424 									if ($SecondClassPos === false) {
3425 										continue;
3426 									}
3427 
3428 									// Get ValueRecord[$i][$j]
3429 									$offs = ($i * $Class2Count * $sizeOfPair) + ($j * $sizeOfPair);
3430 									$this->seek($subtable_offset + 16 + $offs);
3431 
3432 									$Value1 = $this->_getValueRecord($ValueFormat1);
3433 									$Value2 = $this->_getValueRecord($ValueFormat2);
3434 									if ($ValueFormat1) {
3435 										$this->_applyGPOSvaluerecord($ptr, $Value1);
3436 									}
3437 									if ($ValueFormat2) {
3438 										$this->_applyGPOSvaluerecord($matchedpos, $Value2);
3439 										if ($this->debugOTL) {
3440 											$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3441 										}
3442 										return $matchedpos - $ptr + 1;
3443 									}
3444 									if ($this->debugOTL) {
3445 										$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3446 									}
3447 									return $matchedpos - $ptr;
3448 								}
3449 							}
3450 						}
3451 					}
3452 				}
3453 				return 0;
3454 			}
3455 		} ////////////////////////////////////////////////////////////////////////////////
3456 		// LookupType 3: Cursive attachment     Attach cursive glyphs
3457 		////////////////////////////////////////////////////////////////////////////////
3458 		elseif ($Type == 3) {
3459 			$this->skip(4);
3460 			// Need default XAdvance for glyph
3461 			$pdfWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], hexdec($currGlyph)); // DON'T convert back to design units
3462 
3463 			$CPos = $LuCoverage[$currGID];
3464 			$this->skip($CPos * 4);
3465 			$EntryAnchor = $this->read_ushort();
3466 			$ExitAnchor = $this->read_ushort();
3467 			if ($EntryAnchor != 0) {
3468 				$EntryAnchor += $subtable_offset;
3469 				list($x, $y) = $this->_getAnchorTable($EntryAnchor);
3470 				if ($dir == 'RTL') {
3471 					if (round($pdfWidth) == round($x * 1000 / $this->mpdf->CurrentFont['unitsPerEm'])) {
3472 						$x = 0;
3473 					} else {
3474 						$x = $x - ($pdfWidth * $this->mpdf->CurrentFont['unitsPerEm'] / 1000);
3475 					}
3476 				}
3477 
3478 				$this->Entry[$ptr] = ['X' => $x, 'Y' => $y, 'dir' => $dir];
3479 			}
3480 			if ($ExitAnchor != 0) {
3481 				$ExitAnchor += $subtable_offset;
3482 				list($x, $y) = $this->_getAnchorTable($ExitAnchor);
3483 				if ($dir == 'LTR') {
3484 					if (round($pdfWidth) == round($x * 1000 / $this->mpdf->CurrentFont['unitsPerEm'])) {
3485 						$x = 0;
3486 					} else {
3487 						$x = $x - ($pdfWidth * $this->mpdf->CurrentFont['unitsPerEm'] / 1000);
3488 					}
3489 				}
3490 				$this->Exit[$ptr] = ['X' => $x, 'Y' => $y, 'dir' => $dir];
3491 			}
3492 			if ($this->debugOTL) {
3493 				$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3494 			}
3495 			return 1;
3496 		} ////////////////////////////////////////////////////////////////////////////////
3497 		// LookupType 4: MarkToBase attachment  Attach a combining mark to a base glyph
3498 		////////////////////////////////////////////////////////////////////////////////
3499 		elseif ($Type == 4) {
3500 			$MarkCoverage = $subtable_offset + $this->read_ushort();
3501 			//$MarkCoverage is already set in $LuCoverage 00065|00073 etc
3502 			$BaseCoverage = $subtable_offset + $this->read_ushort();
3503 			$ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3504 			$MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3505 			$BaseArray = $subtable_offset + $this->read_ushort(); // Offset to BaseArray table
3506 
3507 			$this->seek($BaseCoverage);
3508 			$BaseGlyphs = implode('|', $this->_getCoverage());
3509 
3510 			$checkpos = $ptr;
3511 			$checkpos--;
3512 
3513 			// ZZZ93
3514 			// In Lohit-Kannada font (old-spec), rules specify a Type 4 GPOS to attach below-forms to base glyph
3515 			// the repositioning does not happen in MS Word, and shouldn't happen comparing with other fonts
3516 			// ?Why not
3517 			// This Fix blocks the GPOS rule if the "mark" is not actually classified as a mark in the GlyphClasses of GDEF
3518 			// but only in Indic old-spec.
3519 			// Test cases: &#xca8;&#xccd;&#xca8;&#xcc1; and &#xc95;&#xccd;&#xcb0;&#xccc;
3520 			if ($this->shaper == 'I' && $is_old_spec && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3521 				return;
3522 			}
3523 
3524 
3525 			// "To identify the base glyph that combines with a mark, the text-processing client must look backward in the glyph string from the mark to the preceding base glyph."
3526 			while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
3527 				$checkpos--;
3528 			}
3529 
3530 			if (isset($this->OTLdata[$checkpos]) && strpos($BaseGlyphs, $this->OTLdata[$checkpos]['hex']) !== false) {
3531 				$matchedpos = $checkpos;
3532 			} else {
3533 				$matchedpos = false;
3534 			}
3535 
3536 			if ($matchedpos !== false) {
3537 				// Get the relevant MarkRecord
3538 				$MarkPos = $LuCoverage[$currGID];
3539 				$MarkRecord = $this->_getMarkRecord($MarkArray, $MarkPos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3540 				//Mark Class is = $MarkRecord['Class']
3541 				// Get the relevant BaseRecord
3542 				$this->seek($BaseArray);
3543 				$BaseCount = $this->read_ushort();
3544 				$BasePos = strpos($BaseGlyphs, $this->OTLdata[$matchedpos]['hex']) / 6;
3545 
3546 				// Move to the BaseRecord we want
3547 				$nSkip = (2 * $BasePos * $ClassCount );
3548 				$this->skip($nSkip);
3549 
3550 				// Read BaseRecord we want for appropriate Class
3551 				$nSkip = 2 * $MarkRecord['Class'];
3552 				$this->skip($nSkip);
3553 				$BaseRecordOffset = $BaseArray + $this->read_ushort();
3554 				list($x, $y) = $this->_getAnchorTable($BaseRecordOffset);
3555 				$BaseRecord = ['AnchorX' => $x, 'AnchorY' => $y]; // e.g. Array ( [AnchorX] => 660 [AnchorY] => 1556 )
3556 				// Need default XAdvance for Base glyph
3557 				$BaseWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3558 				$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $BaseWidth;
3559 				// And any intervening (ignored) characters
3560 				if (($ptr - $matchedpos) > 1) {
3561 					for ($i = $matchedpos + 1; $i < $ptr; $i++) {
3562 						$BaseWidthExtra = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$i]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3563 						$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] += $BaseWidthExtra;
3564 					}
3565 				}
3566 
3567 				// Align to previous Glyph by attachment - so need to add to previous placement values
3568 				$prevXPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'] : 0);
3569 				$prevYPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'] : 0);
3570 
3571 				$this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $BaseRecord['AnchorX'] - $MarkRecord['AnchorX'];
3572 				$this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $BaseRecord['AnchorY'] - $MarkRecord['AnchorY'];
3573 				if ($this->debugOTL) {
3574 					$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3575 				}
3576 				return 1;
3577 			}
3578 			return 0;
3579 		} ////////////////////////////////////////////////////////////////////////////////
3580 		// LookupType 5: MarkToLigature attachment  Attach a combining mark to a ligature
3581 		////////////////////////////////////////////////////////////////////////////////
3582 		elseif ($Type == 5) {
3583 			$MarkCoverage = $subtable_offset + $this->read_ushort();
3584 			//$MarkCoverage is already set in $LuCoverage 00065|00073 etc
3585 			$LigatureCoverage = $subtable_offset + $this->read_ushort();
3586 			$ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3587 			$MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3588 			$LigatureArray = $subtable_offset + $this->read_ushort(); // Offset to LigatureArray table
3589 
3590 			$this->seek($LigatureCoverage);
3591 			$LigatureGlyphs = implode('|', $this->_getCoverage());
3592 
3593 
3594 			$checkpos = $ptr;
3595 			$checkpos--;
3596 
3597 			// "To position a combining mark using a MarkToLigature attachment subtable, the text-processing client must work backward from the mark to the preceding ligature glyph."
3598 			while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
3599 				$checkpos--;
3600 			}
3601 
3602 			if (isset($this->OTLdata[$checkpos]) && strpos($LigatureGlyphs, $this->OTLdata[$checkpos]['hex']) !== false) {
3603 				$matchedpos = $checkpos;
3604 			} else {
3605 				$matchedpos = false;
3606 			}
3607 
3608 			if ($matchedpos !== false) {
3609 				// Get the relevant MarkRecord
3610 				$MarkPos = $LuCoverage[$currGID];
3611 				$MarkRecord = $this->_getMarkRecord($MarkArray, $MarkPos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3612 				//Mark Class is = $MarkRecord['Class']
3613 				// Get the relevant LigatureRecord
3614 				$this->seek($LigatureArray);
3615 				$LigatureCount = $this->read_ushort();
3616 				$LigaturePos = strpos($LigatureGlyphs, $this->OTLdata[$matchedpos]['hex']) / 6;
3617 
3618 				// Move to the LigatureAttach table Record we want
3619 				$nSkip = (2 * $LigaturePos);
3620 				$this->skip($nSkip);
3621 				$LigatureAttachOffset = $LigatureArray + $this->read_ushort();
3622 				$this->seek($LigatureAttachOffset);
3623 				$ComponentCount = $this->read_ushort();
3624 				$offsets = [];
3625 				for ($comp = 0; $comp < $ComponentCount; $comp++) {
3626 					// ComponentRecords
3627 					for ($class = 0; $class < $ClassCount; $class++) {
3628 						$offsets[$comp][$class] = $this->read_ushort();
3629 					}
3630 				}
3631 
3632 				// Get the specific component for this mark attachment
3633 				if (isset($this->assocLigs[$matchedpos]) && isset($this->assocMarks[$ptr]['ligPos']) && $this->assocMarks[$ptr]['ligPos'] == $matchedpos) {
3634 					$component = $this->assocMarks[$ptr]['compID'];
3635 				} else {
3636 					$component = $ComponentCount - 1;
3637 				}
3638 
3639 				$offset = $offsets[$component][$MarkRecord['Class']];
3640 				if ($offset != 0) {
3641 					$LigatureRecordOffset = $offset + $LigatureAttachOffset;
3642 					list($x, $y) = $this->_getAnchorTable($LigatureRecordOffset);
3643 					$LigatureRecord = ['AnchorX' => $x, 'AnchorY' => $y];
3644 
3645 					// Need default XAdvance for Ligature glyph
3646 					$LigatureWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3647 					$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $LigatureWidth;
3648 					// And any intervening (ignored)characters
3649 					if (($ptr - $matchedpos) > 1) {
3650 						for ($i = $matchedpos + 1; $i < $ptr; $i++) {
3651 							$LigatureWidthExtra = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$i]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3652 							$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] += $LigatureWidthExtra;
3653 						}
3654 					}
3655 
3656 					// Align to previous Ligature by attachment - so need to add to previous placement values
3657 					if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'])) {
3658 						$prevXPlacement = $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'];
3659 					} else {
3660 						$prevXPlacement = 0;
3661 					}
3662 					if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'])) {
3663 						$prevYPlacement = $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'];
3664 					} else {
3665 						$prevYPlacement = 0;
3666 					}
3667 
3668 					$this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $LigatureRecord['AnchorX'] - $MarkRecord['AnchorX'];
3669 					$this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $LigatureRecord['AnchorY'] - $MarkRecord['AnchorY'];
3670 					if ($this->debugOTL) {
3671 						$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3672 					}
3673 					return 1;
3674 				}
3675 			}
3676 			return 0;
3677 		} ////////////////////////////////////////////////////////////////////////////////
3678 		// LookupType 6: MarkToMark attachment  Attach a combining mark to another mark
3679 		////////////////////////////////////////////////////////////////////////////////
3680 		elseif ($Type == 6) {
3681 			$Mark1Coverage = $subtable_offset + $this->read_ushort(); // Combining Mark
3682 			//$Mark1Coverage is already set in $LuCoverage 0065|0073 etc
3683 			$Mark2Coverage = $subtable_offset + $this->read_ushort(); // Base Mark
3684 			$ClassCount = $this->read_ushort(); // Number of classes defined for marks = No. of Combining mark1 glyphs in the MarkCoverage table
3685 			$Mark1Array = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3686 			$Mark2Array = $subtable_offset + $this->read_ushort(); // Offset to Mark2Array table
3687 			$this->seek($Mark2Coverage);
3688 			$Mark2Glyphs = implode('|', $this->_getCoverage());
3689 			$checkpos = $ptr;
3690 			$checkpos--;
3691 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
3692 				$checkpos--;
3693 			}
3694 			if (isset($this->OTLdata[$checkpos]) && strpos($Mark2Glyphs, $this->OTLdata[$checkpos]['hex']) !== false) {
3695 				$matchedpos = $checkpos;
3696 			} else {
3697 				$matchedpos = false;
3698 			}
3699 
3700 			if ($matchedpos !== false) {
3701 				// Get the relevant MarkRecord
3702 				$Mark1Pos = $LuCoverage[$currGID];
3703 				$Mark1Record = $this->_getMarkRecord($Mark1Array, $Mark1Pos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3704 				//Mark Class is = $Mark1Record['Class']
3705 				// Get the relevant Mark2Record
3706 				$this->seek($Mark2Array);
3707 				$Mark2Count = $this->read_ushort();
3708 				$Mark2Pos = strpos($Mark2Glyphs, $this->OTLdata[$matchedpos]['hex']) / 6;
3709 
3710 				// Move to the Mark2Record we want
3711 				$nSkip = (2 * $Mark2Pos * $ClassCount );
3712 				$this->skip($nSkip);
3713 
3714 				// Read Mark2Record we want for appropriate Class
3715 				$nSkip = 2 * $Mark1Record['Class'];
3716 				$this->skip($nSkip);
3717 				$Mark2RecordOffset = $Mark2Array + $this->read_ushort();
3718 				list($x, $y) = $this->_getAnchorTable($Mark2RecordOffset);
3719 				$Mark2Record = ['AnchorX' => $x, 'AnchorY' => $y]; // e.g. Array ( [AnchorX] => 660 [AnchorY] => 1556 )
3720 				// Need default XAdvance for Mark2 glyph
3721 				$Mark2Width = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3722 				// IF combining marks are set on different components of a ligature glyph, do not apply this rule
3723 				// Test: arabictypesetting: &#x625;&#x650;&#x644;&#x64e;&#x649;&#x670;&#x653;
3724 				// Test: arabictypesetting: &#x628;&#x651;&#x64e;&#x64a;&#x652;&#x646;&#x64e;&#x643;&#x64f;&#x645;&#x652;
3725 				$prevLig = -1;
3726 				$thisLig = -1;
3727 				$prevComp = -1;
3728 				$thisComp = -1;
3729 				if (isset($this->assocMarks[$matchedpos])) {
3730 					$prevLig = $this->assocMarks[$matchedpos]['ligPos'];
3731 					$prevComp = $this->assocMarks[$matchedpos]['compID'];
3732 				}
3733 				if (isset($this->assocMarks[$ptr])) {
3734 					$thisLig = $this->assocMarks[$ptr]['ligPos'];
3735 					$thisComp = $this->assocMarks[$ptr]['compID'];
3736 				}
3737 
3738 				// However IF Mark2 (first in logical order, i.e. being attached to) is not associated with a base, carry on
3739 				// This happens in Indic when the Mark being attached to e.g. [Halant Ma lig] -> MatraU,  [U+0B4D + U+B2E as E0F5]-> U+0B41 become E135
3740 				if (!defined("OMIT_OTL_FIX_1") || OMIT_OTL_FIX_1 != 1) {
3741 					/* OTL_FIX_1 */
3742 					if (isset($this->assocMarks[$matchedpos]) && ($prevLig != $thisLig || $prevComp != $thisComp )) {
3743 						return 0;
3744 					}
3745 				} else {
3746 					/* Original code */
3747 					if ($prevLig != $thisLig || $prevComp != $thisComp) {
3748 						return 0;
3749 					}
3750 				}
3751 
3752 
3753 				if (!defined("OMIT_OTL_FIX_2") || OMIT_OTL_FIX_2 != 1) {
3754 					/* OTL_FIX_2 */
3755 					if (!isset($this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) || !$this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) {
3756 						$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $Mark2Width;
3757 					}
3758 				}
3759 
3760 				// ZZZ99Q - Test Case font-family: garuda &#xe19;&#xe49;&#xe33;
3761 				if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) && $this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) {
3762 					$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth'];
3763 				}
3764 
3765 				// Align to previous Mark by attachment - so need to add the previous placement values
3766 				$prevXPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'] : 0);
3767 				$prevYPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'] : 0);
3768 				$this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $Mark2Record['AnchorX'] - $Mark1Record['AnchorX'];
3769 				$this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $Mark2Record['AnchorY'] - $Mark1Record['AnchorY'];
3770 				if ($this->debugOTL) {
3771 					$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3772 				}
3773 				return 1;
3774 			}
3775 			return 0;
3776 		} ////////////////////////////////////////////////////////////////////////////////
3777 		// LookupType 7: Context positioning    Position one or more glyphs in context
3778 		////////////////////////////////////////////////////////////////////////////////
3779 		elseif ($Type == 7) {
3780 			//===========
3781 			// Format 1:
3782 			//===========
3783 			if ($PosFormat == 1) {
3784 				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3785 			} //===========
3786 			// Format 2:
3787 			//===========
3788 			elseif ($PosFormat == 2) {
3789 				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
3790 				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
3791 				$PosClassSetCnt = $this->read_ushort();
3792 				$PosClassSetOffset = [];
3793 				for ($b = 0; $b < $PosClassSetCnt; $b++) {
3794 					$offset = $this->read_ushort();
3795 					if ($offset == 0x0000) {
3796 						$PosClassSetOffset[] = $offset;
3797 					} else {
3798 						$PosClassSetOffset[] = $subtable_offset + $offset;
3799 					}
3800 				}
3801 
3802 				$InputClasses = $this->_getClasses($InputClassDefOffset);
3803 
3804 				for ($s = 0; $s < $PosClassSetCnt; $s++) { // $ChainPosClassSet is ordered by input class-may be NULL
3805 					// Select $PosClassSet if currGlyph is in First Input Class
3806 					if ($PosClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
3807 						$this->seek($PosClassSetOffset[$s]);
3808 						$PosClassRuleCnt = $this->read_ushort();
3809 						$PosClassRule = [];
3810 						for ($b = 0; $b < $PosClassRuleCnt; $b++) {
3811 							$PosClassRule[$b] = $PosClassSetOffset[$s] + $this->read_ushort();
3812 						}
3813 
3814 						for ($b = 0; $b < $PosClassRuleCnt; $b++) {  // EACH RULE
3815 							$this->seek($PosClassRule[$b]);
3816 							$InputGlyphCount = $this->read_ushort();
3817 							$PosCount = $this->read_ushort();
3818 
3819 							$Input = [];
3820 							for ($r = 1; $r < $InputGlyphCount; $r++) {
3821 								$Input[$r] = $this->read_ushort();
3822 							}
3823 							$inputClass = $s;
3824 
3825 							$inputGlyphs = [];
3826 							$inputGlyphs[0] = $InputClasses[$inputClass];
3827 
3828 							if ($InputGlyphCount > 1) {
3829 								//  NB starts at 1
3830 								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
3831 									$classindex = $Input[$gcl];
3832 									if (isset($InputClasses[$classindex])) {
3833 										$inputGlyphs[$gcl] = $InputClasses[$classindex];
3834 									} else {
3835 										$inputGlyphs[$gcl] = '';
3836 									}
3837 								}
3838 							}
3839 
3840 							// Class 0 contains all the glyphs NOT in the other classes
3841 							$class0excl = [];
3842 							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
3843 								if (is_array($InputClasses[$gc])) {
3844 									$class0excl = $class0excl + $InputClasses[$gc];
3845 								}
3846 							}
3847 
3848 							$backtrackGlyphs = [];
3849 							$lookaheadGlyphs = [];
3850 
3851 							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl);
3852 							if ($matched) {
3853 								for ($p = 0; $p < $PosCount; $p++) { // EACH LOOKUP
3854 									$SequenceIndex[$p] = $this->read_ushort();
3855 									$LookupListIndex[$p] = $this->read_ushort();
3856 								}
3857 
3858 								for ($p = 0; $p < $PosCount; $p++) {
3859 									// Apply  $LookupListIndex  at   $SequenceIndex
3860 									if ($SequenceIndex[$p] >= $InputGlyphCount) {
3861 										continue;
3862 									}
3863 									$lu = $LookupListIndex[$p];
3864 									$luType = $this->GPOSLookups[$lu]['Type'];
3865 									$luFlag = $this->GPOSLookups[$lu]['Flag'];
3866 									$luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
3867 
3868 									$luptr = $matched[$SequenceIndex[$p]];
3869 									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
3870 									$lucurrGID = $this->OTLdata[$luptr]['uni'];
3871 
3872 									foreach ($this->GPOSLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
3873 										$shift = $this->_applyGPOSsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GPOS_offset + $this->GSUB_length), $luType, $luFlag, $luMarkFilteringSet, $this->LuCoverage[$lu][$luc], $tag, 1, $is_old_spec);
3874 										if ($this->debugOTL && $shift) {
3875 											$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3876 										}
3877 										if ($shift) {
3878 											break;
3879 										}
3880 									}
3881 								}
3882 
3883 								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
3884 									return $shift;
3885 								} /* OTL_FIX_3 */
3886 								else {
3887 									return $InputGlyphCount; // should be + matched ignores in Input Sequence
3888 								}
3889 							}
3890 						}
3891 					}
3892 				}
3893 
3894 				return 0;
3895 			} //===========
3896 			// Format 3:
3897 			//===========
3898 			elseif ($PosFormat == 3) {
3899 				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3900 			} else {
3901 				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . ", Format " . $PosFormat . " not supported.");
3902 			}
3903 		} ////////////////////////////////////////////////////////////////////////////////
3904 		// LookupType 8: Chained Context positioning    Position one or more glyphs in chained context
3905 		////////////////////////////////////////////////////////////////////////////////
3906 		elseif ($Type == 8) {
3907 			//===========
3908 			// Format 1:
3909 			//===========
3910 			if ($PosFormat == 1) {
3911 				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3912 				return 0;
3913 			} //===========
3914 			// Format 2:
3915 			//===========
3916 			elseif ($PosFormat == 2) {
3917 				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
3918 				$BacktrackClassDefOffset = $subtable_offset + $this->read_ushort();
3919 				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
3920 				$LookaheadClassDefOffset = $subtable_offset + $this->read_ushort();
3921 				$ChainPosClassSetCnt = $this->read_ushort();
3922 				$ChainPosClassSetOffset = [];
3923 				for ($b = 0; $b < $ChainPosClassSetCnt; $b++) {
3924 					$offset = $this->read_ushort();
3925 					if ($offset == 0x0000) {
3926 						$ChainPosClassSetOffset[] = $offset;
3927 					} else {
3928 						$ChainPosClassSetOffset[] = $subtable_offset + $offset;
3929 					}
3930 				}
3931 
3932 				$BacktrackClasses = $this->_getClasses($BacktrackClassDefOffset);
3933 				$InputClasses = $this->_getClasses($InputClassDefOffset);
3934 				$LookaheadClasses = $this->_getClasses($LookaheadClassDefOffset);
3935 
3936 				for ($s = 0; $s < $ChainPosClassSetCnt; $s++) { // $ChainPosClassSet is ordered by input class-may be NULL
3937 					// Select $ChainPosClassSet if currGlyph is in First Input Class
3938 					if ($ChainPosClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
3939 						$this->seek($ChainPosClassSetOffset[$s]);
3940 						$ChainPosClassRuleCnt = $this->read_ushort();
3941 						$ChainPosClassRule = [];
3942 						for ($b = 0; $b < $ChainPosClassRuleCnt; $b++) {
3943 							$ChainPosClassRule[$b] = $ChainPosClassSetOffset[$s] + $this->read_ushort();
3944 						}
3945 
3946 						for ($b = 0; $b < $ChainPosClassRuleCnt; $b++) {  // EACH RULE
3947 							$this->seek($ChainPosClassRule[$b]);
3948 							$BacktrackGlyphCount = $this->read_ushort();
3949 							$Backtrack = [];
3950 							for ($r = 0; $r < $BacktrackGlyphCount; $r++) {
3951 								$Backtrack[$r] = $this->read_ushort();
3952 							}
3953 							$InputGlyphCount = $this->read_ushort();
3954 							$Input = [];
3955 							for ($r = 1; $r < $InputGlyphCount; $r++) {
3956 								$Input[$r] = $this->read_ushort();
3957 							}
3958 							$LookaheadGlyphCount = $this->read_ushort();
3959 							$Lookahead = [];
3960 							for ($r = 0; $r < $LookaheadGlyphCount; $r++) {
3961 								$Lookahead[$r] = $this->read_ushort();
3962 							}
3963 
3964 							$inputClass = $s; //???
3965 
3966 							$inputGlyphs = [];
3967 							$inputGlyphs[0] = $InputClasses[$inputClass];
3968 
3969 							if ($InputGlyphCount > 1) {
3970 								//  NB starts at 1
3971 								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
3972 									$classindex = $Input[$gcl];
3973 									if (isset($InputClasses[$classindex])) {
3974 										$inputGlyphs[$gcl] = $InputClasses[$classindex];
3975 									} else {
3976 										$inputGlyphs[$gcl] = '';
3977 									}
3978 								}
3979 							}
3980 
3981 							// Class 0 contains all the glyphs NOT in the other classes
3982 							$class0excl = [];
3983 							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
3984 								if (isset($InputClasses[$gc]) && is_array($InputClasses[$gc])) {
3985 									$class0excl = $class0excl + $InputClasses[$gc];
3986 								}
3987 							}
3988 
3989 							if ($BacktrackGlyphCount) {
3990 								$backtrackGlyphs = [];
3991 								for ($gcl = 0; $gcl < $BacktrackGlyphCount; $gcl++) {
3992 									$classindex = $Backtrack[$gcl];
3993 									if (isset($BacktrackClasses[$classindex])) {
3994 										$backtrackGlyphs[$gcl] = $BacktrackClasses[$classindex];
3995 									} else {
3996 										$backtrackGlyphs[$gcl] = '';
3997 									}
3998 								}
3999 							} else {
4000 								$backtrackGlyphs = [];
4001 							}
4002 
4003 							// Class 0 contains all the glyphs NOT in the other classes
4004 							$bclass0excl = [];
4005 							for ($gc = 1; $gc <= count($BacktrackClasses); $gc++) {
4006 								if (isset($BacktrackClasses[$gc]) && is_array($BacktrackClasses[$gc])) {
4007 									$bclass0excl = $bclass0excl + $BacktrackClasses[$gc];
4008 								}
4009 							}
4010 
4011 							if ($LookaheadGlyphCount) {
4012 								$lookaheadGlyphs = [];
4013 								for ($gcl = 0; $gcl < $LookaheadGlyphCount; $gcl++) {
4014 									$classindex = $Lookahead[$gcl];
4015 									if (isset($LookaheadClasses[$classindex])) {
4016 										$lookaheadGlyphs[$gcl] = $LookaheadClasses[$classindex];
4017 									} else {
4018 										$lookaheadGlyphs[$gcl] = '';
4019 									}
4020 								}
4021 							} else {
4022 								$lookaheadGlyphs = [];
4023 							}
4024 
4025 							// Class 0 contains all the glyphs NOT in the other classes
4026 							$lclass0excl = [];
4027 							for ($gc = 1; $gc <= count($LookaheadClasses); $gc++) {
4028 								if (isset($LookaheadClasses[$gc]) && is_array($LookaheadClasses[$gc])) {
4029 									$lclass0excl = $lclass0excl + $LookaheadClasses[$gc];
4030 								}
4031 							}
4032 
4033 							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl, $bclass0excl, $lclass0excl);
4034 							if ($matched) {
4035 								$PosCount = $this->read_ushort();
4036 								$SequenceIndex = [];
4037 								$LookupListIndex = [];
4038 								for ($p = 0; $p < $PosCount; $p++) { // EACH LOOKUP
4039 									$SequenceIndex[$p] = $this->read_ushort();
4040 									$LookupListIndex[$p] = $this->read_ushort();
4041 								}
4042 
4043 								for ($p = 0; $p < $PosCount; $p++) {
4044 									// Apply  $LookupListIndex  at   $SequenceIndex
4045 									if ($SequenceIndex[$p] >= $InputGlyphCount) {
4046 										continue;
4047 									}
4048 									$lu = $LookupListIndex[$p];
4049 									$luType = $this->GPOSLookups[$lu]['Type'];
4050 									$luFlag = $this->GPOSLookups[$lu]['Flag'];
4051 									$luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
4052 
4053 									$luptr = $matched[$SequenceIndex[$p]];
4054 									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
4055 									$lucurrGID = $this->OTLdata[$luptr]['uni'];
4056 
4057 									foreach ($this->GPOSLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
4058 										$shift = $this->_applyGPOSsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GPOS_offset + $this->GSUB_length), $luType, $luFlag, $luMarkFilteringSet, $this->LuCoverage[$lu][$luc], $tag, 1, $is_old_spec);
4059 										if ($this->debugOTL && $shift) {
4060 											$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
4061 										}
4062 										if ($shift) {
4063 											break;
4064 										}
4065 									}
4066 								}
4067 
4068 								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
4069 									return $shift;
4070 								} /* OTL_FIX_3 */
4071 								else {
4072 									return $InputGlyphCount; // should be + matched ignores in Input Sequence
4073 								}
4074 							}
4075 						}
4076 					}
4077 				}
4078 
4079 				return 0;
4080 			} //===========
4081 			// Format 3:
4082 			//===========
4083 			elseif ($PosFormat == 3) {
4084 				$BacktrackGlyphCount = $this->read_ushort();
4085 				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
4086 					$CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
4087 				}
4088 				$InputGlyphCount = $this->read_ushort();
4089 				for ($b = 0; $b < $InputGlyphCount; $b++) {
4090 					$CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
4091 				}
4092 				$LookaheadGlyphCount = $this->read_ushort();
4093 				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
4094 					$CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
4095 				}
4096 				$PosCount = $this->read_ushort();
4097 				$save_pos = $this->_pos; // Save the point just after PosCount
4098 
4099 				$CoverageBacktrackGlyphs = [];
4100 				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
4101 					$this->seek($CoverageBacktrackOffset[$b]);
4102 					$glyphs = $this->_getCoverage();
4103 					$CoverageBacktrackGlyphs[$b] = implode("|", $glyphs);
4104 				}
4105 				$CoverageInputGlyphs = [];
4106 				for ($b = 0; $b < $InputGlyphCount; $b++) {
4107 					$this->seek($CoverageInputOffset[$b]);
4108 					$glyphs = $this->_getCoverage();
4109 					$CoverageInputGlyphs[$b] = implode("|", $glyphs);
4110 				}
4111 				$CoverageLookaheadGlyphs = [];
4112 				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
4113 					$this->seek($CoverageLookaheadOffset[$b]);
4114 					$glyphs = $this->_getCoverage();
4115 					$CoverageLookaheadGlyphs[$b] = implode("|", $glyphs);
4116 				}
4117 				$matched = $this->checkContextMatchMultiple($CoverageInputGlyphs, $CoverageBacktrackGlyphs, $CoverageLookaheadGlyphs, $ignore, $ptr);
4118 				if ($matched) {
4119 					$this->seek($save_pos); // Return to just after PosCount
4120 					for ($p = 0; $p < $PosCount; $p++) {
4121 						// PosLookupRecord
4122 						$PosLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
4123 						$PosLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
4124 					}
4125 					for ($p = 0; $p < $PosCount; $p++) {
4126 						// Apply  $PosLookupRecord[$p]['LookupListIndex']  at   $PosLookupRecord[$p]['SequenceIndex']
4127 						if ($PosLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) {
4128 							continue;
4129 						}
4130 						$lu = $PosLookupRecord[$p]['LookupListIndex'];
4131 						$luType = $this->GPOSLookups[$lu]['Type'];
4132 						$luFlag = $this->GPOSLookups[$lu]['Flag'];
4133 						if (isset($this->GPOSLookups[$lu]['MarkFilteringSet'])) {
4134 							$luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
4135 						} else {
4136 							$luMarkFilteringSet = '';
4137 						}
4138 
4139 						$luptr = $matched[$PosLookupRecord[$p]['SequenceIndex']];
4140 						$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
4141 						$lucurrGID = $this->OTLdata[$luptr]['uni'];
4142 
4143 						foreach ($this->GPOSLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
4144 							$shift = $this->_applyGPOSsubtable($lu, $luc, $luptr, $lucurrGlyph, $lucurrGID, ($lusubtable_offset - $this->GPOS_offset + $this->GSUB_length), $luType, $luFlag, $luMarkFilteringSet, $this->LuCoverage[$lu][$luc], $tag, 1, $is_old_spec);
4145 							if ($this->debugOTL && $shift) {
4146 								$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
4147 							}
4148 							if ($shift) {
4149 								break;
4150 							}
4151 						}
4152 					}
4153 				}
4154 			} else {
4155 				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . ", Format " . $PosFormat . " not supported.");
4156 			}
4157 		} else {
4158 			throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " not supported.");
4159 		}
4160 	}
4161 
4162 	//////////////////////////////////////////////////////////////////////////////////
4163 	// GPOS / GSUB / GCOM (common) functions
4164 	//////////////////////////////////////////////////////////////////////////////////
4165 	private function checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr)
4166 	{
4167 		// Input etc are single numbers - GSUB Format 6.1
4168 		// Input starts with (1=>xxx)
4169 		// return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
4170 
4171 		$current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
4172 
4173 		// BACKTRACK
4174 		$checkpos = $ptr;
4175 		for ($i = 0; $i < count($Backtrack); $i++) {
4176 			$checkpos--;
4177 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4178 				$checkpos--;
4179 			}
4180 			// If outside scope of current syllable - return no match
4181 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4182 				return false;
4183 			} elseif (!isset($this->OTLdata[$checkpos]) || $this->OTLdata[$checkpos]['uni'] != $Backtrack[$i]) {
4184 				return false;
4185 			}
4186 		}
4187 
4188 		// INPUT
4189 		$matched = [0 => $ptr];
4190 		$checkpos = $ptr;
4191 		for ($i = 1; $i < count($Input); $i++) {
4192 			$checkpos++;
4193 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4194 				$checkpos++;
4195 			}
4196 			// If outside scope of current syllable - return no match
4197 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4198 				return false;
4199 			} elseif (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == $Input[$i]) {
4200 				$matched[] = $checkpos;
4201 			} else {
4202 				return false;
4203 			}
4204 		}
4205 
4206 		// LOOKAHEAD
4207 		for ($i = 0; $i < count($Lookahead); $i++) {
4208 			$checkpos++;
4209 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4210 				$checkpos++;
4211 			}
4212 			// If outside scope of current syllable - return no match
4213 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4214 				return false;
4215 			} elseif (!isset($this->OTLdata[$checkpos]) || $this->OTLdata[$checkpos]['uni'] != $Lookahead[$i]) {
4216 				return false;
4217 			}
4218 		}
4219 
4220 		return $matched;
4221 	}
4222 
4223 	private function checkContextMatchMultiple($Input, $Backtrack, $Lookahead, $ignore, $ptr, $class0excl = '', $bclass0excl = '', $lclass0excl = '')
4224 	{
4225 		// Input etc are string/array of glyph strings  - GSUB Format 5.2, 5.3, 6.2, 6.3, GPOS Format 7.2, 7.3, 8.2, 8.3
4226 		// Input starts with (1=>xxx)
4227 		// return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
4228 		// $class0excl is the string of glyphs in all classes except Class 0 (GSUB 5.2, 6.2, GPOS 7.2, 8.2)
4229 		// $bclass0excl & $lclass0excl are the same for lookahead and backtrack (GSUB 6.2, GPOS 8.2)
4230 
4231 		$current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
4232 
4233 		// BACKTRACK
4234 		$checkpos = $ptr;
4235 		for ($i = 0; $i < count($Backtrack); $i++) {
4236 			$checkpos--;
4237 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4238 				$checkpos--;
4239 			}
4240 			// If outside scope of current syllable - return no match
4241 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4242 				return false;
4243 			} // If Class 0 specified, matches anything NOT in $bclass0excl
4244 			elseif (!$Backtrack[$i] && isset($this->OTLdata[$checkpos]) && strpos($bclass0excl, $this->OTLdata[$checkpos]['hex']) !== false) {
4245 				return false;
4246 			} elseif (!isset($this->OTLdata[$checkpos]) || strpos($Backtrack[$i], $this->OTLdata[$checkpos]['hex']) === false) {
4247 				return false;
4248 			}
4249 		}
4250 
4251 		// INPUT
4252 		$matched = [0 => $ptr];
4253 		$checkpos = $ptr;
4254 		for ($i = 1; $i < count($Input); $i++) { // Start at 1 - already matched the first InputGlyph
4255 			$checkpos++;
4256 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4257 				$checkpos++;
4258 			}
4259 			// If outside scope of current syllable - return no match
4260 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4261 				return false;
4262 			} // If Input Class 0 specified, matches anything NOT in $class0excl
4263 			elseif (!$Input[$i] && isset($this->OTLdata[$checkpos]) && strpos($class0excl, $this->OTLdata[$checkpos]['hex']) === false) {
4264 				$matched[] = $checkpos;
4265 			} elseif (isset($this->OTLdata[$checkpos]) && strpos($Input[$i], $this->OTLdata[$checkpos]['hex']) !== false) {
4266 				$matched[] = $checkpos;
4267 			} else {
4268 				return false;
4269 			}
4270 		}
4271 
4272 		// LOOKAHEAD
4273 		for ($i = 0; $i < count($Lookahead); $i++) {
4274 			$checkpos++;
4275 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4276 				$checkpos++;
4277 			}
4278 			// If outside scope of current syllable - return no match
4279 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4280 				return false;
4281 			} // If Class 0 specified, matches anything NOT in $lclass0excl
4282 			elseif (!$Lookahead[$i] && isset($this->OTLdata[$checkpos]) && strpos($lclass0excl, $this->OTLdata[$checkpos]['hex']) !== false) {
4283 				return false;
4284 			} elseif (!isset($this->OTLdata[$checkpos]) || strpos($Lookahead[$i], $this->OTLdata[$checkpos]['hex']) === false) {
4285 				return false;
4286 			}
4287 		}
4288 		return $matched;
4289 	}
4290 
4291 	private function checkContextMatchMultipleUni($Input, $Backtrack, $Lookahead, $ignore, $ptr, $class0excl = [], $bclass0excl = [], $lclass0excl = [])
4292 	{
4293 		// Input etc are array of glyphs - GSUB Format 5.2, 5.3, 6.2, 6.3, GPOS Format 7.2, 7.3, 8.2, 8.3
4294 		// Input starts with (1=>xxx)
4295 		// return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
4296 		// $class0excl is array of glyphs in all classes except Class 0 (GSUB 5.2, 6.2, GPOS 7.2, 8.2)
4297 		// $bclass0excl & $lclass0excl are the same for lookahead and backtrack (GSUB 6.2, GPOS 8.2)
4298 
4299 		$current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
4300 
4301 		// BACKTRACK
4302 		$checkpos = $ptr;
4303 		for ($i = 0; $i < count($Backtrack); $i++) {
4304 			$checkpos--;
4305 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4306 				$checkpos--;
4307 			}
4308 			// If outside scope of current syllable - return no match
4309 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4310 				return false;
4311 			} // If Class 0 specified, matches anything NOT in $bclass0excl
4312 			elseif (!$Backtrack[$i] && isset($this->OTLdata[$checkpos]) && isset($bclass0excl[$this->OTLdata[$checkpos]['uni']])) {
4313 				return false;
4314 			} elseif (!isset($this->OTLdata[$checkpos]) || !isset($Backtrack[$i][$this->OTLdata[$checkpos]['uni']])) {
4315 				return false;
4316 			}
4317 		}
4318 
4319 		// INPUT
4320 		$matched = [0 => $ptr];
4321 		$checkpos = $ptr;
4322 		for ($i = 1; $i < count($Input); $i++) { // Start at 1 - already matched the first InputGlyph
4323 			$checkpos++;
4324 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4325 				$checkpos++;
4326 			}
4327 			// If outside scope of current syllable - return no match
4328 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4329 				return false;
4330 			} // If Input Class 0 specified, matches anything NOT in $class0excl
4331 			elseif (!$Input[$i] && isset($this->OTLdata[$checkpos]) && !isset($class0excl[$this->OTLdata[$checkpos]['uni']])) {
4332 				$matched[] = $checkpos;
4333 			} elseif (isset($this->OTLdata[$checkpos]) && isset($Input[$i][$this->OTLdata[$checkpos]['uni']])) {
4334 				$matched[] = $checkpos;
4335 			} else {
4336 				return false;
4337 			}
4338 		}
4339 
4340 		// LOOKAHEAD
4341 		for ($i = 0; $i < count($Lookahead); $i++) {
4342 			$checkpos++;
4343 			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4344 				$checkpos++;
4345 			}
4346 			// If outside scope of current syllable - return no match
4347 			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4348 				return false;
4349 			} // If Class 0 specified, matches anything NOT in $lclass0excl
4350 			elseif (!$Lookahead[$i] && isset($this->OTLdata[$checkpos]) && isset($lclass0excl[$this->OTLdata[$checkpos]['uni']])) {
4351 				return false;
4352 			} elseif (!isset($this->OTLdata[$checkpos]) || !isset($Lookahead[$i][$this->OTLdata[$checkpos]['uni']])) {
4353 				return false;
4354 			}
4355 		}
4356 		return $matched;
4357 	}
4358 
4359 	private function _getClassDefinitionTable($offset)
4360 	{
4361 		if (isset($this->LuDataCache[$this->fontkey][$offset])) {
4362 			$GlyphByClass = $this->LuDataCache[$this->fontkey][$offset];
4363 		} else {
4364 			$this->seek($offset);
4365 			$ClassFormat = $this->read_ushort();
4366 			$GlyphClass = [];
4367 			$GlyphByClass = [];
4368 			if ($ClassFormat == 1) {
4369 				$StartGlyph = $this->read_ushort();
4370 				$GlyphCount = $this->read_ushort();
4371 				for ($i = 0; $i < $GlyphCount; $i++) {
4372 					$GlyphClass[$i]['startGlyphID'] = $StartGlyph + $i;
4373 					$GlyphClass[$i]['endGlyphID'] = $StartGlyph + $i;
4374 					$GlyphClass[$i]['class'] = $this->read_ushort();
4375 					for ($g = $GlyphClass[$i]['startGlyphID']; $g <= $GlyphClass[$i]['endGlyphID']; $g++) {
4376 						$GlyphByClass[$GlyphClass[$i]['class']][] = $this->glyphToChar($g);
4377 					}
4378 				}
4379 			} elseif ($ClassFormat == 2) {
4380 				$tableCount = $this->read_ushort();
4381 				for ($i = 0; $i < $tableCount; $i++) {
4382 					$GlyphClass[$i]['startGlyphID'] = $this->read_ushort();
4383 					$GlyphClass[$i]['endGlyphID'] = $this->read_ushort();
4384 					$GlyphClass[$i]['class'] = $this->read_ushort();
4385 					for ($g = $GlyphClass[$i]['startGlyphID']; $g <= $GlyphClass[$i]['endGlyphID']; $g++) {
4386 						$GlyphByClass[$GlyphClass[$i]['class']][] = $this->glyphToChar($g);
4387 					}
4388 				}
4389 			}
4390 			ksort($GlyphByClass);
4391 			$this->LuDataCache[$this->fontkey][$offset] = $GlyphByClass;
4392 		}
4393 		return $GlyphByClass;
4394 	}
4395 
4396 	private function count_bits($n)
4397 	{
4398 		for ($c = 0; $n; $c++) {
4399 			$n &= $n - 1; // clear the least significant bit set
4400 		}
4401 		return $c;
4402 	}
4403 
4404 	private function _getValueRecord($ValueFormat)
4405 	{
4406 	// Common ValueRecord for GPOS
4407 		// Only returns 3 possible: $vra['XPlacement'] $vra['YPlacement'] $vra['XAdvance']
4408 		$vra = [];
4409 		// Horizontal adjustment for placement - in design units
4410 		if (($ValueFormat & 0x0001) == 0x0001) {
4411 			$vra['XPlacement'] = $this->read_short();
4412 		}
4413 		// Vertical adjustment for placement - in design units
4414 		if (($ValueFormat & 0x0002) == 0x0002) {
4415 			$vra['YPlacement'] = $this->read_short();
4416 		}
4417 		// Horizontal adjustment for advance - in design units (only used for horizontal writing)
4418 		if (($ValueFormat & 0x0004) == 0x0004) {
4419 			$vra['XAdvance'] = $this->read_short();
4420 		}
4421 		// Vertical adjustment for advance - in design units (only used for vertical writing)
4422 		if (($ValueFormat & 0x0008) == 0x0008) {
4423 			$this->read_short();
4424 		}
4425 		// Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)
4426 		if (($ValueFormat & 0x0010) == 0x0010) {
4427 			$this->read_ushort();
4428 		}
4429 		// Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)
4430 		if (($ValueFormat & 0x0020) == 0x0020) {
4431 			$this->read_ushort();
4432 		}
4433 		// Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)
4434 		if (($ValueFormat & 0x0040) == 0x0040) {
4435 			$this->read_ushort();
4436 		}
4437 		// Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)
4438 		if (($ValueFormat & 0x0080) == 0x0080) {
4439 			$this->read_ushort();
4440 		}
4441 		return $vra;
4442 	}
4443 
4444 	private function _getAnchorTable($offset = 0)
4445 	{
4446 		if ($offset) {
4447 			$this->seek($offset);
4448 		}
4449 		$AnchorFormat = $this->read_ushort();
4450 		$XCoordinate = $this->read_short();
4451 		$YCoordinate = $this->read_short();
4452 		// Format 2 specifies additional link to contour point; Format 3 additional Device table
4453 		return [$XCoordinate, $YCoordinate];
4454 	}
4455 
4456 	private function _getMarkRecord($offset, $MarkPos)
4457 	{
4458 		$this->seek($offset);
4459 		$MarkCount = $this->read_ushort();
4460 		$this->skip($MarkPos * 4);
4461 		$Class = $this->read_ushort();
4462 		$MarkAnchor = $offset + $this->read_ushort();  // = Offset to anchor table
4463 		list($x, $y) = $this->_getAnchorTable($MarkAnchor);
4464 		$MarkRecord = ['Class' => $Class, 'AnchorX' => $x, 'AnchorY' => $y];
4465 		return $MarkRecord;
4466 	}
4467 
4468 	private function _getGCOMignoreString($flag, $MarkFilteringSet)
4469 	{
4470 		// If ignoreFlag set, combine all ignore glyphs into -> "(?:( 0FBA1| 0FBA2| 0FBA3)*)"
4471 		// else "()"
4472 		// for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup
4473 		$str = "";
4474 		$ignoreflag = 0;
4475 
4476 		// Flag & 0xFF?? = MarkAttachmentType
4477 		if ($flag & 0xFF00) {
4478 			// "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
4479 			// $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
4480 			$MarkAttachmentType = $flag >> 8;
4481 			$ignoreflag = $flag;
4482 			$str = $this->MarkAttachmentType[$MarkAttachmentType];
4483 		}
4484 
4485 		// Flag & 0x0010 = UseMarkFilteringSet
4486 		if ($flag & 0x0010) {
4487 			throw new \Mpdf\MpdfException("This font [" . $this->fontkey . "] contains MarkGlyphSets - Not tested yet");
4488 			// Change also in ttfontsuni.php
4489 			if ($MarkFilteringSet == '') {
4490 				throw new \Mpdf\MpdfException("This font [" . $this->fontkey . "] contains MarkGlyphSets - but MarkFilteringSet not set");
4491 			}
4492 			$str = $this->MarkGlyphSets[$MarkFilteringSet];
4493 		}
4494 
4495 		// If Ignore Marks set, supercedes any above
4496 		// Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
4497 		if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) {
4498 			$ignoreflag = 8;
4499 			$str = $this->GlyphClassMarks;
4500 		}
4501 
4502 		// Flag & 0x0004 = Ignore Ligatures
4503 		if (($flag & 0x0004) == 0x0004) {
4504 			$ignoreflag += 4;
4505 			if ($str) {
4506 				$str .= "|";
4507 			}
4508 			$str .= $this->GlyphClassLigatures;
4509 		}
4510 		// Flag & 0x0002 = Ignore BaseGlyphs
4511 		if (($flag & 0x0002) == 0x0002) {
4512 			$ignoreflag += 2;
4513 			if ($str) {
4514 				$str .= "|";
4515 			}
4516 			$str .= $this->GlyphClassBases;
4517 		}
4518 		if ($str) {
4519 			return "((?:(?:" . $str . "))*)";
4520 		} else {
4521 			return "()";
4522 		}
4523 	}
4524 
4525 	private function _checkGCOMignore($flag, $glyph, $MarkFilteringSet)
4526 	{
4527 		$ignore = false;
4528 		// Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
4529 		if (($flag & 0x0008 && ($flag & 0xFF00) == 0) && strpos($this->GlyphClassMarks, $glyph)) {
4530 			$ignore = true;
4531 		}
4532 		if (($flag & 0x0004) && strpos($this->GlyphClassLigatures, $glyph)) {
4533 			$ignore = true;
4534 		}
4535 		if (($flag & 0x0002) && strpos($this->GlyphClassBases, $glyph)) {
4536 			$ignore = true;
4537 		}
4538 		// Flag & 0xFF?? = MarkAttachmentType
4539 		if ($flag & 0xFF00) {
4540 			// "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
4541 			// $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
4542 			if (strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)) {
4543 				$ignore = true;
4544 			}
4545 		}
4546 		// Flag & 0x0010 = UseMarkFilteringSet
4547 		if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) {
4548 			$ignore = true;
4549 		}
4550 		return $ignore;
4551 	}
4552 
4553 	/**
4554 	 * Bidi algorithm
4555 	 *
4556 	 * These functions are called from mpdf after GSUB/GPOS has taken place
4557 	 * At this stage the bidi-type is in string form
4558 	 *
4559 	 * Bidirectional Character Types
4560 	 * =============================
4561 	 * Type  Description     General Scope
4562 	 * Strong
4563 	 * L     Left-to-Right       LRM, most alphabetic, syllabic, Han ideographs, non-European or non-Arabic digits, ...
4564 	 * LRE   Left-to-Right Embedding LRE
4565 	 * LRO   Left-to-Right Override  LRO
4566 	 * R     Right-to-Left       RLM, Hebrew alphabet, and related punctuation
4567 	 * AL    Right-to-Left Arabic    Arabic, Thaana, and Syriac alphabets, most punctuation specific to those scripts, ...
4568 	 * RLE   Right-to-Left Embedding RLE
4569 	 * RLO   Right-to-Left Override  RLO
4570 	 * Weak
4571 	 * PDF   Pop Directional Format      PDF
4572 	 * EN    European Number             European digits, Eastern Arabic-Indic digits, ...
4573 	 * ES    European Number Separator   Plus sign, minus sign
4574 	 * ET    European Number Terminator  Degree sign, currency symbols, ...
4575 	 * AN    Arabic Number           Arabic-Indic digits, Arabic decimal and thousands separators, ...
4576 	 * CS    Common Number Separator     Colon, comma, full stop (period), No-break space, ...
4577 	 * NSM   Nonspacing Mark             Characters marked Mn (Nonspacing_Mark) and Me (Enclosing_Mark) in the Unicode Character Database
4578 	 * BN    Boundary Neutral            Default ignorables, non-characters, and control characters, other than those explicitly given other types.
4579 	 * Neutral
4580 	 * B     Paragraph Separator     Paragraph separator, appropriate Newline Functions, higher-level protocol paragraph determination
4581 	 * S     Segment Separator   Tab
4582 	 * WS    Whitespace          Space, figure space, line separator, form feed, General Punctuation spaces, ...
4583 	 * ON    Other Neutrals      All other characters, including OBJECT REPLACEMENT CHARACTER
4584 	 */
4585 	public function bidiSort($ta, $str, $dir, &$chunkOTLdata, $useGPOS)
4586 	{
4587 
4588 		$pel = 0; // paragraph embedding level
4589 		$maxlevel = 0;
4590 		$numchars = count($chunkOTLdata['char_data']);
4591 
4592 		// Set the initial paragraph embedding level
4593 		if ($dir == 'rtl') {
4594 			$pel = 1;
4595 		} else {
4596 			$pel = 0;
4597 		}
4598 
4599 		// X1. Begin by setting the current embedding level to the paragraph embedding level. Set the directional override status to neutral.
4600 		// Current Embedding Level
4601 		$cel = $pel;
4602 		// directional override status (-1 is Neutral)
4603 		$dos = -1;
4604 		$remember = [];
4605 
4606 		// Array of characters data
4607 		$chardata = [];
4608 
4609 		// Process each character iteratively, applying rules X2 through X9. Only embedding levels from 0 to 61 are valid in this phase.
4610 		// In the resolution of levels in rules I1 and I2, the maximum embedding level of 62 can be reached.
4611 		for ($i = 0; $i < $numchars; ++$i) {
4612 			if ($chunkOTLdata['char_data'][$i]['uni'] == 8235) { // RLE
4613 				// X2. With each RLE, compute the least greater odd embedding level.
4614 				//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
4615 				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4616 				$next_level = $cel + ($cel % 2) + 1;
4617 				if ($next_level < 62) {
4618 					$remember[] = ['num' => 8235, 'cel' => $cel, 'dos' => $dos];
4619 					$cel = $next_level;
4620 					$dos = -1;
4621 				}
4622 			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8234) { // LRE
4623 				// X3. With each LRE, compute the least greater even embedding level.
4624 				//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
4625 				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4626 				$next_level = $cel + 2 - ($cel % 2);
4627 				if ($next_level < 62) {
4628 					$remember[] = ['num' => 8234, 'cel' => $cel, 'dos' => $dos];
4629 					$cel = $next_level;
4630 					$dos = -1;
4631 				}
4632 			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8238) { // RLO
4633 				// X4. With each RLO, compute the least greater odd embedding level.
4634 				//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to right-to-left.
4635 				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4636 				$next_level = $cel + ($cel % 2) + 1;
4637 				if ($next_level < 62) {
4638 					$remember[] = ['num' => 8238, 'cel' => $cel, 'dos' => $dos];
4639 					$cel = $next_level;
4640 					$dos = Ucdn::BIDI_CLASS_R;
4641 				}
4642 			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8237) { // LRO
4643 				// X5. With each LRO, compute the least greater even embedding level.
4644 				//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to left-to-right.
4645 				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4646 				$next_level = $cel + 2 - ($cel % 2);
4647 				if ($next_level < 62) {
4648 					$remember[] = ['num' => 8237, 'cel' => $cel, 'dos' => $dos];
4649 					$cel = $next_level;
4650 					$dos = Ucdn::BIDI_CLASS_L;
4651 				}
4652 			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8236) { // PDF
4653 				// X7. With each PDF, determine the matching embedding or override code. If there was a valid matching code, restore (pop) the last remembered (pushed) embedding level and directional override.
4654 				if (count($remember)) {
4655 					$last = count($remember) - 1;
4656 					if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
4657 						($remember[$last]['num'] == 8237)) {
4658 						$match = array_pop($remember);
4659 						$cel = $match['cel'];
4660 						$dos = $match['dos'];
4661 					}
4662 				}
4663 			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 10) { // NEW LINE
4664 				// Reset to start values
4665 				$cel = $pel;
4666 				$dos = -1;
4667 				$remember = [];
4668 			} else {
4669 				// X6. For all types besides RLE, LRE, RLO, LRO, and PDF:
4670 				//  a. Set the level of the current character to the current embedding level.
4671 				//  b. When the directional override status is not neutral, reset the current character type to directional override status.
4672 				if ($dos != -1) {
4673 					$chardir = $dos;
4674 				} else {
4675 					$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
4676 				}
4677 				// stores string characters and other information
4678 				if (isset($chunkOTLdata['GPOSinfo'][$i])) {
4679 					$gpos = $chunkOTLdata['GPOSinfo'][$i];
4680 				} else {
4681 					$gpos = '';
4682 				}
4683 				$chardata[] = ['char' => $chunkOTLdata['char_data'][$i]['uni'], 'level' => $cel, 'type' => $chardir, 'group' => $chunkOTLdata['group'][$i], 'GPOSinfo' => $gpos];
4684 			}
4685 		}
4686 
4687 		$numchars = count($chardata);
4688 
4689 		// X8. All explicit directional embeddings and overrides are completely terminated at the end of each paragraph.
4690 		// Paragraph separators are not included in the embedding.
4691 		// X9. Remove all RLE, LRE, RLO, LRO, and PDF codes.
4692 		// This is effectively done by only saving other codes to chardata
4693 		// X10. Determine the start-of-sequence (sor) and end-of-sequence (eor) types, either L or R, for each isolating run sequence. These depend on the higher of the two levels on either side of the sequence boundary:
4694 		// For sor, compare the level of the first character in the sequence with the level of the character preceding it in the paragraph or if there is none, with the paragraph embedding level.
4695 		// For eor, compare the level of the last character in the sequence with the level of the character following it in the paragraph or if there is none, with the paragraph embedding level.
4696 		// If the higher level is odd, the sor or eor is R; otherwise, it is L.
4697 
4698 		$prelevel = $pel;
4699 		$postlevel = $pel;
4700 		$cel = $prelevel; // current embedding level
4701 		for ($i = 0; $i < $numchars; ++$i) {
4702 			$level = $chardata[$i]['level'];
4703 			if ($i == 0) {
4704 				$left = $prelevel;
4705 			} else {
4706 				$left = $chardata[$i - 1]['level'];
4707 			}
4708 			if ($i == ($numchars - 1)) {
4709 				$right = $postlevel;
4710 			} else {
4711 				$right = $chardata[$i + 1]['level'];
4712 			}
4713 			$chardata[$i]['sor'] = max($left, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
4714 			$chardata[$i]['eor'] = max($right, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
4715 		}
4716 
4717 
4718 
4719 		// 3.3.3 Resolving Weak Types
4720 		// Weak types are now resolved one level run at a time. At level run boundaries where the type of the character on the other side of the boundary is required, the type assigned to sor or eor is used.
4721 		// Nonspacing marks are now resolved based on the previous characters.
4722 		// W1. Examine each nonspacing mark (NSM) in the level run, and change the type of the NSM to the type of the previous character. If the NSM is at the start of the level run, it will get the type of sor.
4723 		for ($i = 0; $i < $numchars; ++$i) {
4724 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_NSM) {
4725 				if ($i == 0 || $chardata[$i]['level'] != $chardata[$i - 1]['level']) {
4726 					$chardata[$i]['type'] = $chardata[$i]['sor'];
4727 				} else {
4728 					$chardata[$i]['type'] = $chardata[($i - 1)]['type'];
4729 				}
4730 			}
4731 		}
4732 
4733 		// W2. Search backward from each instance of a European number until the first strong type (R, L, AL, or sor) is found. If an AL is found, change the type of the European number to Arabic number.
4734 		$prevlevel = -1;
4735 		$levcount = 0;
4736 		for ($i = 0; $i < $numchars; ++$i) {
4737 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN) {
4738 				$found = false;
4739 				for ($j = $levcount; $j >= 0; $j--) {
4740 					if ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_AL) {
4741 						$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
4742 						$found = true;
4743 						break;
4744 					} elseif (($chardata[$j]['type'] == Ucdn::BIDI_CLASS_L) || ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_R)) {
4745 						$found = true;
4746 						break;
4747 					}
4748 				}
4749 			}
4750 			if ($chardata[$i]['level'] != $prevlevel) {
4751 				$levcount = 0;
4752 			} else {
4753 				++$levcount;
4754 			}
4755 			$prevlevel = $chardata[$i]['level'];
4756 		}
4757 
4758 		// W3. Change all ALs to R.
4759 		for ($i = 0; $i < $numchars; ++$i) {
4760 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL) {
4761 				$chardata[$i]['type'] = Ucdn::BIDI_CLASS_R;
4762 			}
4763 		}
4764 
4765 		// W4. A single European separator between two European numbers changes to a European number. A single common separator between two numbers of the same type changes to that type.
4766 		for ($i = 1; $i < $numchars; ++$i) {
4767 			if (($i + 1) < $numchars && $chardata[($i)]['level'] == $chardata[($i + 1)]['level'] && $chardata[($i)]['level'] == $chardata[($i - 1)]['level']) {
4768 				if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN && $chardata[($i + 1)]['type'] == Ucdn::BIDI_CLASS_EN) {
4769 					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4770 				} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN && $chardata[($i + 1)]['type'] == Ucdn::BIDI_CLASS_EN) {
4771 					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4772 				} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_AN && $chardata[($i + 1)]['type'] == Ucdn::BIDI_CLASS_AN) {
4773 					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
4774 				}
4775 			}
4776 		}
4777 
4778 		// W5. A sequence of European terminators adjacent to European numbers changes to all European numbers.
4779 		for ($i = 0; $i < $numchars; ++$i) {
4780 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) {
4781 				if ($i > 0 && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN && $chardata[($i)]['level'] == $chardata[($i - 1)]['level']) {
4782 					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4783 				} else {
4784 					$j = $i + 1;
4785 					while ($j < $numchars && $chardata[$j]['level'] == $chardata[$i]['level']) {
4786 						if ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_EN) {
4787 							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4788 							break;
4789 						} elseif ($chardata[$j]['type'] != Ucdn::BIDI_CLASS_ET) {
4790 							break;
4791 						}
4792 						++$j;
4793 					}
4794 				}
4795 			}
4796 		}
4797 
4798 		// W6. Otherwise, separators and terminators change to Other Neutral.
4799 		for ($i = 0; $i < $numchars; ++$i) {
4800 			if (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS)) {
4801 				$chardata[$i]['type'] = Ucdn::BIDI_CLASS_ON;
4802 			}
4803 		}
4804 
4805 		//W7. Search backward from each instance of a European number until the first strong type (R, L, or sor) is found. If an L is found, then change the type of the European number to L.
4806 		for ($i = 0; $i < $numchars; ++$i) {
4807 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN) {
4808 				if ($i == 0) { // Start of Level run
4809 					if ($chardata[$i]['sor'] == Ucdn::BIDI_CLASS_L) {
4810 						$chardata[$i]['type'] = $chardata[$i]['sor'];
4811 					}
4812 				} else {
4813 					for ($j = $i - 1; $j >= 0; $j--) {
4814 						if ($chardata[$j]['level'] != $chardata[$i]['level']) { // Level run boundary
4815 							if ($chardata[$j + 1]['sor'] == Ucdn::BIDI_CLASS_L) {
4816 								$chardata[$i]['type'] = $chardata[$j + 1]['sor'];
4817 							}
4818 							break;
4819 						} elseif ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_L) {
4820 							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_L;
4821 							break;
4822 						} elseif ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_R) {
4823 							break;
4824 						}
4825 					}
4826 				}
4827 			}
4828 		}
4829 
4830 		// N1. A sequence of neutrals takes the direction of the surrounding strong text if the text on both sides has the same direction. European and Arabic numbers act as if they were R in terms of their influence on neutrals. Start-of-level-run (sor) and end-of-level-run (eor) are used at level run boundaries.
4831 		for ($i = 0; $i < $numchars; ++$i) {
4832 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS) {
4833 				$left = -1;
4834 				// LEFT
4835 				if ($i == 0) {  // first char
4836 					$left = $chardata[($i)]['sor'];
4837 				} elseif ($chardata[($i - 1)]['level'] != $chardata[($i)]['level']) {  // run boundary
4838 					$left = $chardata[($i)]['sor'];
4839 				} elseif ($chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_L) {
4840 					$left = Ucdn::BIDI_CLASS_L;
4841 				} elseif ($chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_R || $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN || $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_AN) {
4842 					$left = Ucdn::BIDI_CLASS_R;
4843 				}
4844 				// RIGHT
4845 				$right = -1;
4846 				$j = $i;
4847 				// move to the right of any following neutrals OR hit a run boundary
4848 				while (($chardata[$j]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$j]['type'] == Ucdn::BIDI_CLASS_WS) && $j <= ($numchars - 1)) {
4849 					if ($j == ($numchars - 1)) {  // last char
4850 						$right = $chardata[($j)]['eor'];
4851 						break;
4852 					} elseif ($chardata[($j + 1)]['level'] != $chardata[($j)]['level']) {  // run boundary
4853 						$right = $chardata[($j)]['eor'];
4854 						break;
4855 					} elseif ($chardata[($j + 1)]['type'] == Ucdn::BIDI_CLASS_L) {
4856 						$right = Ucdn::BIDI_CLASS_L;
4857 						break;
4858 					} elseif ($chardata[($j + 1)]['type'] == Ucdn::BIDI_CLASS_R || $chardata[($j + 1)]['type'] == Ucdn::BIDI_CLASS_EN || $chardata[($j + 1)]['type'] == Ucdn::BIDI_CLASS_AN) {
4859 						$right = Ucdn::BIDI_CLASS_R;
4860 						break;
4861 					}
4862 					$j++;
4863 				}
4864 				if ($left > -1 && $left == $right) {
4865 					$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
4866 					$chardata[$i]['type'] = $left;
4867 				}
4868 			}
4869 		}
4870 
4871 		// N2. Any remaining neutrals take the embedding direction
4872 		for ($i = 0; $i < $numchars; ++$i) {
4873 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS) {
4874 				$chardata[$i]['type'] = ($chardata[$i]['level'] % 2) ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
4875 				$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
4876 			}
4877 		}
4878 
4879 		// I1. For all characters with an even (left-to-right) embedding direction, those of type R go up one level and those of type AN or EN go up two levels.
4880 		// I2. For all characters with an odd (right-to-left) embedding direction, those of type L, EN or AN go up one level.
4881 		for ($i = 0; $i < $numchars; ++$i) {
4882 			$odd = $chardata[$i]['level'] % 2;
4883 			if ($odd) {
4884 				if (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
4885 					$chardata[$i]['level'] += 1;
4886 				}
4887 			} else {
4888 				if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_R) {
4889 					$chardata[$i]['level'] += 1;
4890 				} elseif (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
4891 					$chardata[$i]['level'] += 2;
4892 				}
4893 			}
4894 			$maxlevel = max($chardata[$i]['level'], $maxlevel);
4895 		}
4896 
4897 		// NB
4898 		//  Separate into lines at this point************
4899 		//
4900 		// L1. On each line, reset the embedding level of the following characters to the paragraph embedding level:
4901 		//  1. Segment separators (Tab) 'S',
4902 		//  2. Paragraph separators 'B',
4903 		//  3. Any sequence of whitespace characters 'WS' preceding a segment separator or paragraph separator, and
4904 		//  4. Any sequence of whitespace characters 'WS' at the end of the line.
4905 		//  The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2*******
4906 		//  Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line.
4907 
4908 		for ($i = ($numchars - 1); $i > 0; $i--) {
4909 			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS || (isset($chardata[$i]['orig_type']) && $chardata[$i]['orig_type'] == Ucdn::BIDI_CLASS_WS)) {
4910 				$chardata[$i]['level'] = $pel;
4911 			} else {
4912 				break;
4913 			}
4914 		}
4915 
4916 
4917 		// L2. From the highest level found in the text to the lowest odd level on each line, including intermediate levels not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
4918 		for ($j = $maxlevel; $j > 0; $j--) {
4919 			$ordarray = [];
4920 			$revarr = [];
4921 			$onlevel = false;
4922 			for ($i = 0; $i < $numchars; ++$i) {
4923 				if ($chardata[$i]['level'] >= $j) {
4924 					$onlevel = true;
4925 
4926 					// L4. A character is depicted by a mirrored glyph if and only if (a) the resolved directionality of that character is R, and (b) the Bidi_Mirrored property value of that character is true.
4927 					if (isset(Ucdn::$mirror_pairs[$chardata[$i]['char']]) && $chardata[$i]['type'] == Ucdn::BIDI_CLASS_R) {
4928 						$chardata[$i]['char'] = Ucdn::$mirror_pairs[$chardata[$i]['char']];
4929 					}
4930 
4931 					$revarr[] = $chardata[$i];
4932 				} else {
4933 					if ($onlevel) {
4934 						$revarr = array_reverse($revarr);
4935 						$ordarray = array_merge($ordarray, $revarr);
4936 						$revarr = [];
4937 						$onlevel = false;
4938 					}
4939 					$ordarray[] = $chardata[$i];
4940 				}
4941 			}
4942 			if ($onlevel) {
4943 				$revarr = array_reverse($revarr);
4944 				$ordarray = array_merge($ordarray, $revarr);
4945 			}
4946 			$chardata = $ordarray;
4947 		}
4948 
4949 		$group = '';
4950 		$e = '';
4951 		$GPOS = [];
4952 		$cctr = 0;
4953 		$rtl_content = 0x0;
4954 		foreach ($chardata as $cd) {
4955 			$e .= UtfString::code2utf($cd['char']);
4956 			$group .= $cd['group'];
4957 			if ($useGPOS && is_array($cd['GPOSinfo'])) {
4958 				$GPOS[$cctr] = $cd['GPOSinfo'];
4959 				$GPOS[$cctr]['wDir'] = ($cd['level'] % 2) ? 'RTL' : 'LTR';
4960 			}
4961 			if ($cd['type'] == Ucdn::BIDI_CLASS_L) {
4962 				$rtl_content |= 1;
4963 			} elseif ($cd['type'] == Ucdn::BIDI_CLASS_R) {
4964 				$rtl_content |= 2;
4965 			}
4966 			$cctr++;
4967 		}
4968 
4969 
4970 		$chunkOTLdata['group'] = $group;
4971 		if ($useGPOS) {
4972 			$chunkOTLdata['GPOSinfo'] = $GPOS;
4973 		}
4974 
4975 		return [$e, $rtl_content];
4976 	}
4977 
4978 	/**
4979 	 * The following versions for BidiSort work on amalgamated chunks to process the whole paragraph
4980 	 *
4981 	 * Firstly set the level in the OTLdata - called from fn printbuffer() [_bidiPrepare]
4982 	 * Secondly re-order - called from fn writeFlowingBlock and FinishFlowingBlock, when already divided into lines. [_bidiReorder]
4983 	 */
4984 	public function bidiPrepare(&$para, $dir)
4985 	{
4986 
4987 		// Set the initial paragraph embedding level
4988 		$pel = 0; // paragraph embedding level
4989 		if ($dir == 'rtl') {
4990 			$pel = 1;
4991 		}
4992 
4993 		// X1. Begin by setting the current embedding level to the paragraph embedding level. Set the directional override status to neutral.
4994 		// Current Embedding Level
4995 		$cel = $pel;
4996 		// directional override status (-1 is Neutral)
4997 		$dos = -1;
4998 		$remember = [];
4999 		$controlchars = false;
5000 		$strongrtl = false;
5001 		$diid = 0; // direction isolate ID
5002 		$dictr = 0; // direction isolate counter
5003 		// Process each character iteratively, applying rules X2 through X9. Only embedding levels from 0 to 61 are valid in this phase.
5004 		// In the resolution of levels in rules I1 and I2, the maximum embedding level of 62 can be reached.
5005 		$numchunks = count($para);
5006 		for ($nc = 0; $nc < $numchunks; $nc++) {
5007 			$chunkOTLdata = & $para[$nc][18];
5008 
5009 			$numchars = count($chunkOTLdata['char_data']);
5010 			for ($i = 0; $i < $numchars; ++$i) {
5011 				if ($chunkOTLdata['char_data'][$i]['uni'] == 8235) { // RLE
5012 					// X2. With each RLE, compute the least greater odd embedding level.
5013 					//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
5014 					//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
5015 					$next_level = $cel + ($cel % 2) + 1;
5016 					if ($next_level < 62) {
5017 						$remember[] = ['num' => 8235, 'cel' => $cel, 'dos' => $dos];
5018 						$cel = $next_level;
5019 						$dos = -1;
5020 						$controlchars = true;
5021 					}
5022 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8234) { // LRE
5023 					// X3. With each LRE, compute the least greater even embedding level.
5024 					//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to neutral.
5025 					//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
5026 					$next_level = $cel + 2 - ($cel % 2);
5027 					if ($next_level < 62) {
5028 						$remember[] = ['num' => 8234, 'cel' => $cel, 'dos' => $dos];
5029 						$cel = $next_level;
5030 						$dos = -1;
5031 						$controlchars = true;
5032 					}
5033 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8238) { // RLO
5034 					// X4. With each RLO, compute the least greater odd embedding level.
5035 					//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to right-to-left.
5036 					//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
5037 					$next_level = $cel + ($cel % 2) + 1;
5038 					if ($next_level < 62) {
5039 						$remember[] = ['num' => 8238, 'cel' => $cel, 'dos' => $dos];
5040 						$cel = $next_level;
5041 						$dos = Ucdn::BIDI_CLASS_R;
5042 						$controlchars = true;
5043 					}
5044 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8237) { // LRO
5045 					// X5. With each LRO, compute the least greater even embedding level.
5046 					//  a. If this new level would be valid, then this embedding code is valid. Remember (push) the current embedding level and override status. Reset the current level to this new level, and reset the override status to left-to-right.
5047 					//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
5048 					$next_level = $cel + 2 - ($cel % 2);
5049 					if ($next_level < 62) {
5050 						$remember[] = ['num' => 8237, 'cel' => $cel, 'dos' => $dos];
5051 						$cel = $next_level;
5052 						$dos = Ucdn::BIDI_CLASS_L;
5053 						$controlchars = true;
5054 					}
5055 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8236) { // PDF
5056 					// X7. With each PDF, determine the matching embedding or override code. If there was a valid matching code, restore (pop) the last remembered (pushed) embedding level and directional override.
5057 					if (count($remember)) {
5058 						$last = count($remember) - 1;
5059 						if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
5060 							($remember[$last]['num'] == 8237)) {
5061 							$match = array_pop($remember);
5062 							$cel = $match['cel'];
5063 							$dos = $match['dos'];
5064 						}
5065 					}
5066 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8294 || $chunkOTLdata['char_data'][$i]['uni'] == 8295 ||
5067 					$chunkOTLdata['char_data'][$i]['uni'] == 8296) { // LRI // RLI // FSI
5068 					// X5a. With each RLI:
5069 					// X5b. With each LRI:
5070 					// X5c. With each FSI, apply rules P2 and P3 for First Strong character
5071 					//  Set the RLI/LRI/FSI embedding level to the embedding level of the last entry on the directional status stack.
5072 					if ($dos != -1) {
5073 						$chardir = $dos;
5074 					} else {
5075 						$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
5076 					}
5077 					$chunkOTLdata['char_data'][$i]['level'] = $cel;
5078 					$chunkOTLdata['char_data'][$i]['type'] = $chardir;
5079 					$chunkOTLdata['char_data'][$i]['diid'] = $diid;
5080 
5081 					$fsi = '';
5082 					// X5c. With each FSI, apply rules P2 and P3 within the isolate run for First Strong character
5083 					if ($chunkOTLdata['char_data'][$i]['uni'] == 8296) { // FSI
5084 						$lvl = 0;
5085 						$nc2 = $nc;
5086 						$i2 = $i;
5087 						while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) {  // while not at end of last chunk
5088 							$i2++;
5089 							if ($i2 >= count($para[$nc2][18]['char_data'])) {
5090 								$nc2++;
5091 								$i2 = 0;
5092 							}
5093 							if ($lvl > 0) {
5094 								continue;
5095 							}
5096 							if ($para[$nc2][18]['char_data'][$i2]['uni'] == 8294 || $para[$nc2][18]['char_data'][$i2]['uni'] == 8295 || $para[$nc2][18]['char_data'][$i2]['uni'] == 8296) {
5097 								$lvl++;
5098 								continue;
5099 							}
5100 							if ($para[$nc2][18]['char_data'][$i2]['uni'] == 8297) {
5101 								$lvl--;
5102 								if ($lvl < 0) {
5103 									break;
5104 								}
5105 							}
5106 							if ($para[$nc2][18]['char_data'][$i2]['bidi_class'] === Ucdn::BIDI_CLASS_L || $para[$nc2][18]['char_data'][$i2]['bidi_class'] == Ucdn::BIDI_CLASS_AL || $para[$nc2][18]['char_data'][$i2]['bidi_class'] === Ucdn::BIDI_CLASS_R) {
5107 								$fsi = $para[$nc2][18]['char_data'][$i2]['bidi_class'];
5108 								break;
5109 							}
5110 						}
5111 						// if fsi not found, fsi is same as paragraph embedding level
5112 						if (!$fsi && $fsi !== 0) {
5113 							if ($pel == 1) {
5114 								$fsi = Ucdn::BIDI_CLASS_R;
5115 							} else {
5116 								$fsi = Ucdn::BIDI_CLASS_L;
5117 							}
5118 						}
5119 					}
5120 
5121 					if ($chunkOTLdata['char_data'][$i]['uni'] == 8294 || $fsi === Ucdn::BIDI_CLASS_L) { // LRI or FSI-L
5122 						//  Compute the least even embedding level greater than the embedding level of the last entry on the directional status stack.
5123 						$next_level = $cel + 2 - ($cel % 2);
5124 					} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8295 || $fsi == Ucdn::BIDI_CLASS_R || $fsi == Ucdn::BIDI_CLASS_AL) { // RLI or FSI-R
5125 						//  Compute the least odd embedding level greater than the embedding level of the last entry on the directional status stack.
5126 						$next_level = $cel + ($cel % 2) + 1;
5127 					}
5128 
5129 
5130 					//  Increment the isolate count by one, and push an entry consisting of the new embedding level,
5131 					//  neutral directional override status, and true directional isolate status onto the directional status stack.
5132 					$remember[] = ['num' => $chunkOTLdata['char_data'][$i]['uni'], 'cel' => $cel, 'dos' => $dos, 'diid' => $diid];
5133 					$cel = $next_level;
5134 					$dos = -1;
5135 					$diid = ++$dictr; // Set new direction isolate ID after incrementing direction isolate counter
5136 
5137 					$controlchars = true;
5138 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8297) { // PDI
5139 					// X6a. With each PDI, perform the following steps:
5140 					//  Pop the last entry from the directional status stack and decrement the isolate count by one.
5141 					while (count($remember)) {
5142 						$last = count($remember) - 1;
5143 						if (($remember[$last]['num'] == 8294) || ($remember[$last]['num'] == 8295) || ($remember[$last]['num'] == 8296)) {
5144 							$match = array_pop($remember);
5145 							$cel = $match['cel'];
5146 							$dos = $match['dos'];
5147 							$diid = $match['diid'];
5148 							break;
5149 						} // End/close any open embedding states not explicitly closed during the isolate
5150 						elseif (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
5151 							($remember[$last]['num'] == 8237)) {
5152 							$match = array_pop($remember);
5153 						}
5154 					}
5155 					//  In all cases, set the PDI’s level to the embedding level of the last entry on the directional status stack left after the steps above.
5156 					//  NB The level assigned to an isolate initiator is always the same as that assigned to the matching PDI.
5157 					if ($dos != -1) {
5158 						$chardir = $dos;
5159 					} else {
5160 						$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
5161 					}
5162 					$chunkOTLdata['char_data'][$i]['level'] = $cel;
5163 					$chunkOTLdata['char_data'][$i]['type'] = $chardir;
5164 					$chunkOTLdata['char_data'][$i]['diid'] = $diid;
5165 					$controlchars = true;
5166 				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 10) { // NEW LINE
5167 					// Reset to start values
5168 					$cel = $pel;
5169 					$dos = -1;
5170 					$remember = [];
5171 				} else {
5172 					// X6. For all types besides RLE, LRE, RLO, LRO, and PDF:
5173 					//  a. Set the level of the current character to the current embedding level.
5174 					//  b. When the directional override status is not neutral, reset the current character type to directional override status.
5175 					if ($dos != -1) {
5176 						$chardir = $dos;
5177 					} else {
5178 						$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
5179 						if ($chardir == Ucdn::BIDI_CLASS_R || $chardir == Ucdn::BIDI_CLASS_AL) {
5180 							$strongrtl = true;
5181 						}
5182 					}
5183 					$chunkOTLdata['char_data'][$i]['level'] = $cel;
5184 					$chunkOTLdata['char_data'][$i]['type'] = $chardir;
5185 					$chunkOTLdata['char_data'][$i]['diid'] = $diid;
5186 				}
5187 			}
5188 			// X8. All explicit directional embeddings and overrides are completely terminated at the end of each paragraph.
5189 			// Paragraph separators are not included in the embedding.
5190 			// X9. Remove all RLE, LRE, RLO, LRO, and PDF codes.
5191 			if ($controlchars) {
5192 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xaa");
5193 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xab");
5194 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xac");
5195 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xad");
5196 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xae");
5197 				preg_replace("/\x{202a}-\x{202e}/u", '', $para[$nc][0]);
5198 			}
5199 		}
5200 
5201 		// Remove any blank chunks made by removing directional codes
5202 		$numchunks = count($para);
5203 		for ($nc = ($numchunks - 1); $nc >= 0; $nc--) {
5204 			if (count($para[$nc][18]['char_data']) == 0) {
5205 				array_splice($para, $nc, 1);
5206 			}
5207 		}
5208 		if ($dir != 'rtl' && !$strongrtl && !$controlchars) {
5209 			return;
5210 		}
5211 
5212 		$numchunks = count($para);
5213 
5214 		// X10. Determine the start-of-sequence (sor) and end-of-sequence (eor) types, either L or R, for each isolating run sequence. These depend on the higher of the two levels on either side of the sequence boundary:
5215 		// For sor, compare the level of the first character in the sequence with the level of the character preceding it in the paragraph or if there is none, with the paragraph embedding level.
5216 		// For eor, compare the level of the last character in the sequence with the level of the character following it in the paragraph or if there is none, with the paragraph embedding level.
5217 		// If the higher level is odd, the sor or eor is R; otherwise, it is L.
5218 
5219 		for ($ir = 0; $ir <= $dictr; $ir++) {
5220 			$prelevel = $pel;
5221 			$postlevel = $pel;
5222 			$firstchar = true;
5223 			for ($nc = 0; $nc < $numchunks; $nc++) {
5224 				$chardata = & $para[$nc][18]['char_data'];
5225 				$numchars = count($chardata);
5226 				for ($i = 0; $i < $numchars; ++$i) {
5227 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5228 						continue;
5229 					} // Ignore characters in a different isolate run
5230 					$right = $postlevel;
5231 					$nc2 = $nc;
5232 					$i2 = $i;
5233 					while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) {  // while not at end of last chunk
5234 						$i2++;
5235 						if ($i2 >= count($para[$nc2][18]['char_data'])) {
5236 							$nc2++;
5237 							$i2 = 0;
5238 						}
5239 
5240 						if (isset($para[$nc2][18]['char_data'][$i2]['diid']) && $para[$nc2][18]['char_data'][$i2]['diid'] == $ir) {
5241 							$right = $para[$nc2][18]['char_data'][$i2]['level'];
5242 							break;
5243 						}
5244 					}
5245 
5246 					$level = $chardata[$i]['level'];
5247 					if ($firstchar || $level != $prelevel) {
5248 						$chardata[$i]['sor'] = max($prelevel, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
5249 					}
5250 					if (($nc == ($numchunks - 1) && $i == ($numchars - 1)) || $level != $right) {
5251 						$chardata[$i]['eor'] = max($right, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
5252 					}
5253 					$prelevel = $level;
5254 					$firstchar = false;
5255 				}
5256 			}
5257 		}
5258 
5259 
5260 		// 3.3.3 Resolving Weak Types
5261 		// Weak types are now resolved one level run at a time. At level run boundaries where the type of the character on the other side of the boundary is required, the type assigned to sor or eor is used.
5262 		// Nonspacing marks are now resolved based on the previous characters.
5263 		// W1. Examine each nonspacing mark (NSM) in the level run, and change the type of the NSM to the type of the previous character. If the NSM is at the start of the level run, it will get the type of sor.
5264 		for ($ir = 0; $ir <= $dictr; $ir++) {
5265 			$prevtype = 0;
5266 			for ($nc = 0; $nc < $numchunks; $nc++) {
5267 				$chardata = & $para[$nc][18]['char_data'];
5268 				$numchars = count($chardata);
5269 				for ($i = 0; $i < $numchars; ++$i) {
5270 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5271 						continue;
5272 					} // Ignore characters in a different isolate run
5273 					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_NSM) {
5274 						if (isset($chardata[$i]['sor'])) {
5275 							$chardata[$i]['type'] = $chardata[$i]['sor'];
5276 						} else {
5277 							$chardata[$i]['type'] = $prevtype;
5278 						}
5279 					}
5280 					$prevtype = $chardata[$i]['type'];
5281 				}
5282 			}
5283 		}
5284 
5285 		// W2. Search backward from each instance of a European number until the first strong type (R, L, AL or sor) is found. If an AL is found, change the type of the European number to Arabic number.
5286 		for ($ir = 0; $ir <= $dictr; $ir++) {
5287 			$laststrongtype = -1;
5288 			for ($nc = 0; $nc < $numchunks; $nc++) {
5289 				$chardata = & $para[$nc][18]['char_data'];
5290 				$numchars = count($chardata);
5291 				for ($i = 0; $i < $numchars; ++$i) {
5292 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5293 						continue;
5294 					} // Ignore characters in a different isolate run
5295 					if (isset($chardata[$i]['sor'])) {
5296 						$laststrongtype = $chardata[$i]['sor'];
5297 					}
5298 					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN && $laststrongtype == Ucdn::BIDI_CLASS_AL) {
5299 						$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
5300 					}
5301 					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_R || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL) {
5302 						$laststrongtype = $chardata[$i]['type'];
5303 					}
5304 				}
5305 			}
5306 		}
5307 
5308 
5309 		// W3. Change all ALs to R.
5310 		for ($nc = 0; $nc < $numchunks; $nc++) {
5311 			$chardata = & $para[$nc][18]['char_data'];
5312 			$numchars = count($chardata);
5313 			for ($i = 0; $i < $numchars; ++$i) {
5314 				if (isset($chardata[$i]['type']) && $chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL) {
5315 					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_R;
5316 				}
5317 			}
5318 		}
5319 
5320 
5321 		// W4. A single European separator between two European numbers changes to a European number. A single common separator between two numbers of the same type changes to that type.
5322 		for ($ir = 0; $ir <= $dictr; $ir++) {
5323 			$prevtype = -1;
5324 			$nexttype = -1;
5325 			for ($nc = 0; $nc < $numchunks; $nc++) {
5326 				$chardata = & $para[$nc][18]['char_data'];
5327 				$numchars = count($chardata);
5328 				for ($i = 0; $i < $numchars; ++$i) {
5329 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5330 						continue;
5331 					} // Ignore characters in a different isolate run
5332 					// Get next type
5333 					$nexttype = -1;
5334 					$nc2 = $nc;
5335 					$i2 = $i;
5336 					while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) {  // while not at end of last chunk
5337 						$i2++;
5338 						if ($i2 >= count($para[$nc2][18]['char_data'])) {
5339 							$nc2++;
5340 							$i2 = 0;
5341 						}
5342 
5343 						if (isset($para[$nc2][18]['char_data'][$i2]['diid']) && $para[$nc2][18]['char_data'][$i2]['diid'] == $ir) {
5344 							$nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5345 							break;
5346 						}
5347 					}
5348 
5349 					if (!isset($chardata[$i]['sor']) && !isset($chardata[$i]['eor'])) {
5350 						if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES && $prevtype == Ucdn::BIDI_CLASS_EN && $nexttype == Ucdn::BIDI_CLASS_EN) {
5351 							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5352 						} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $prevtype == Ucdn::BIDI_CLASS_EN && $nexttype == Ucdn::BIDI_CLASS_EN) {
5353 							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5354 						} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $prevtype == Ucdn::BIDI_CLASS_AN && $nexttype == Ucdn::BIDI_CLASS_AN) {
5355 							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
5356 						}
5357 					}
5358 					$prevtype = $chardata[$i]['type'];
5359 				}
5360 			}
5361 		}
5362 
5363 		// W5. A sequence of European terminators adjacent to European numbers changes to all European numbers.
5364 		for ($ir = 0; $ir <= $dictr; $ir++) {
5365 			$prevtype = -1;
5366 			$nexttype = -1;
5367 			for ($nc = 0; $nc < $numchunks; $nc++) {
5368 				$chardata = & $para[$nc][18]['char_data'];
5369 				$numchars = count($chardata);
5370 				for ($i = 0; $i < $numchars; ++$i) {
5371 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5372 						continue;
5373 					} // Ignore characters in a different isolate run
5374 					if (isset($chardata[$i]['sor'])) {
5375 						$prevtype = $chardata[$i]['sor'];
5376 					}
5377 
5378 					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) {
5379 						if ($prevtype == Ucdn::BIDI_CLASS_EN) {
5380 							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5381 						} elseif (!isset($chardata[$i]['eor'])) {
5382 							$nexttype = -1;
5383 							$nc2 = $nc;
5384 							$i2 = $i;
5385 							while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) { // while not at end of last chunk
5386 								$i2++;
5387 								if ($i2 >= count($para[$nc2][18]['char_data'])) {
5388 									$nc2++;
5389 									$i2 = 0;
5390 								}
5391 								if (!isset($para[$nc2][18]['char_data'][$i2]['diid']) || $para[$nc2][18]['char_data'][$i2]['diid'] != $ir) {
5392 									continue;
5393 								}
5394 								$nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5395 								if (isset($para[$nc2][18]['char_data'][$i2]['sor'])) {
5396 									break;
5397 								}
5398 								if ($nexttype == Ucdn::BIDI_CLASS_EN) {
5399 									$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5400 									break;
5401 								} elseif ($nexttype != Ucdn::BIDI_CLASS_ET) {
5402 									break;
5403 								}
5404 							}
5405 						}
5406 					}
5407 					$prevtype = $chardata[$i]['type'];
5408 				}
5409 			}
5410 		}
5411 
5412 		// W6. Otherwise, separators and terminators change to Other Neutral.
5413 		for ($nc = 0; $nc < $numchunks; $nc++) {
5414 			$chardata = & $para[$nc][18]['char_data'];
5415 			$numchars = count($chardata);
5416 			for ($i = 0; $i < $numchars; ++$i) {
5417 				if (isset($chardata[$i]['type']) && (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS))) {
5418 					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_ON;
5419 				}
5420 			}
5421 		}
5422 
5423 		//W7. Search backward from each instance of a European number until the first strong type (R, L, or sor) is found. If an L is found, then change the type of the European number to L.
5424 		for ($ir = 0; $ir <= $dictr; $ir++) {
5425 			$laststrongtype = -1;
5426 			for ($nc = 0; $nc < $numchunks; $nc++) {
5427 				$chardata = & $para[$nc][18]['char_data'];
5428 				$numchars = count($chardata);
5429 				for ($i = 0; $i < $numchars; ++$i) {
5430 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5431 						continue;
5432 					} // Ignore characters in a different isolate run
5433 					if (isset($chardata[$i]['sor'])) {
5434 						$laststrongtype = $chardata[$i]['sor'];
5435 					}
5436 					if (isset($chardata[$i]['type']) && $chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN && $laststrongtype == Ucdn::BIDI_CLASS_L) {
5437 						$chardata[$i]['type'] = Ucdn::BIDI_CLASS_L;
5438 					}
5439 					if (isset($chardata[$i]['type']) && ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_R || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL)) {
5440 						$laststrongtype = $chardata[$i]['type'];
5441 					}
5442 				}
5443 			}
5444 		}
5445 
5446 		// N1. A sequence of neutrals takes the direction of the surrounding strong text if the text on both sides has the same direction. European and Arabic numbers act as if they were R in terms of their influence on neutrals. Start-of-level-run (sor) and end-of-level-run (eor) are used at level run boundaries.
5447 		for ($ir = 0; $ir <= $dictr; $ir++) {
5448 			$laststrongtype = -1;
5449 			for ($nc = 0; $nc < $numchunks; $nc++) {
5450 				$chardata = & $para[$nc][18]['char_data'];
5451 				$numchars = count($chardata);
5452 				for ($i = 0; $i < $numchars; ++$i) {
5453 					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5454 						continue;
5455 					} // Ignore characters in a different isolate run
5456 					if (isset($chardata[$i]['sor'])) {
5457 						$laststrongtype = $chardata[$i]['sor'];
5458 					}
5459 					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS) {
5460 						$left = -1;
5461 						// LEFT
5462 						if ($laststrongtype == Ucdn::BIDI_CLASS_R || $laststrongtype == Ucdn::BIDI_CLASS_EN || $laststrongtype == Ucdn::BIDI_CLASS_AN) {
5463 							$left = Ucdn::BIDI_CLASS_R;
5464 						} elseif ($laststrongtype == Ucdn::BIDI_CLASS_L) {
5465 							$left = Ucdn::BIDI_CLASS_L;
5466 						}
5467 						// RIGHT
5468 						$right = -1;
5469 						// move to the right of any following neutrals OR hit a run boundary
5470 
5471 						if (isset($chardata[$i]['eor'])) {
5472 							$right = $chardata[$i]['eor'];
5473 						} else {
5474 							$nexttype = -1;
5475 							$nc2 = $nc;
5476 							$i2 = $i;
5477 							while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) { // while not at end of last chunk
5478 								$i2++;
5479 								if ($i2 >= count($para[$nc2][18]['char_data'])) {
5480 									$nc2++;
5481 									$i2 = 0;
5482 								}
5483 								if (!isset($para[$nc2][18]['char_data'][$i2]['diid']) || $para[$nc2][18]['char_data'][$i2]['diid'] != $ir) {
5484 									continue;
5485 								}
5486 								$nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5487 								if ($nexttype == Ucdn::BIDI_CLASS_R || $nexttype == Ucdn::BIDI_CLASS_EN || $nexttype == Ucdn::BIDI_CLASS_AN) {
5488 									$right = Ucdn::BIDI_CLASS_R;
5489 									break;
5490 								} elseif ($nexttype == Ucdn::BIDI_CLASS_L) {
5491 									$right = Ucdn::BIDI_CLASS_L;
5492 									break;
5493 								} elseif (isset($para[$nc2][18]['char_data'][$i2]['eor'])) {
5494 									$right = $para[$nc2][18]['char_data'][$i2]['eor'];
5495 									break;
5496 								}
5497 							}
5498 						}
5499 
5500 						if ($left > -1 && $left == $right) {
5501 							$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
5502 							$chardata[$i]['type'] = $left;
5503 						}
5504 					} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_R || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) {
5505 						$laststrongtype = $chardata[$i]['type'];
5506 					}
5507 				}
5508 			}
5509 		}
5510 
5511 		// N2. Any remaining neutrals take the embedding direction
5512 		for ($nc = 0; $nc < $numchunks; $nc++) {
5513 			$chardata = & $para[$nc][18]['char_data'];
5514 			$numchars = count($chardata);
5515 			for ($i = 0; $i < $numchars; ++$i) {
5516 				if (isset($chardata[$i]['type']) && ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS)) {
5517 					$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
5518 					$chardata[$i]['type'] = ($chardata[$i]['level'] % 2) ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
5519 				}
5520 			}
5521 		}
5522 
5523 		// I1. For all characters with an even (left-to-right) embedding direction, those of type R go up one level and those of type AN or EN go up two levels.
5524 		// I2. For all characters with an odd (right-to-left) embedding direction, those of type L, EN or AN go up one level.
5525 		for ($nc = 0; $nc < $numchunks; $nc++) {
5526 			$chardata = & $para[$nc][18]['char_data'];
5527 			$numchars = count($chardata);
5528 			for ($i = 0; $i < $numchars; ++$i) {
5529 				if (isset($chardata[$i]['level'])) {
5530 					$odd = $chardata[$i]['level'] % 2;
5531 					if ($odd) {
5532 						if (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
5533 							$chardata[$i]['level'] += 1;
5534 						}
5535 					} else {
5536 						if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_R) {
5537 							$chardata[$i]['level'] += 1;
5538 						} elseif (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
5539 							$chardata[$i]['level'] += 2;
5540 						}
5541 					}
5542 				}
5543 			}
5544 		}
5545 
5546 		// Remove Isolate formatters
5547 		$numchunks = count($para);
5548 		if ($controlchars) {
5549 			for ($nc = 0; $nc < $numchunks; $nc++) {
5550 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa6");
5551 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa7");
5552 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa8");
5553 				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa9");
5554 				preg_replace("/\x{2066}-\x{2069}/u", '', $para[$nc][0]);
5555 			}
5556 			// Remove any blank chunks made by removing directional codes
5557 			for ($nc = ($numchunks - 1); $nc >= 0; $nc--) {
5558 				if (count($para[$nc][18]['char_data']) == 0) {
5559 					array_splice($para, $nc, 1);
5560 				}
5561 			}
5562 		}
5563 	}
5564 
5565 	/**
5566 	 * Reorder, once divided into lines
5567 	 */
5568 	public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir)
5569 	{
5570 
5571 		$bidiData = [];
5572 
5573 		// First combine into one array (and get the highest level in use)
5574 		$numchunks = count($content);
5575 		$maxlevel = 0;
5576 		for ($nc = 0; $nc < $numchunks; $nc++) {
5577 			$numchars = isset($cOTLdata[$nc]['char_data']) ? count($cOTLdata[$nc]['char_data']) : 0;
5578 			for ($i = 0; $i < $numchars; ++$i) {
5579 				$carac = [];
5580 				if (isset($cOTLdata[$nc]['GPOSinfo'][$i])) {
5581 					$carac['GPOSinfo'] = $cOTLdata[$nc]['GPOSinfo'][$i];
5582 				}
5583 				$carac['uni'] = $cOTLdata[$nc]['char_data'][$i]['uni'];
5584 				if (isset($cOTLdata[$nc]['char_data'][$i]['type'])) {
5585 					$carac['type'] = $cOTLdata[$nc]['char_data'][$i]['type'];
5586 				}
5587 				if (isset($cOTLdata[$nc]['char_data'][$i]['level'])) {
5588 					$carac['level'] = $cOTLdata[$nc]['char_data'][$i]['level'];
5589 				}
5590 				if (isset($cOTLdata[$nc]['char_data'][$i]['orig_type'])) {
5591 					$carac['orig_type'] = $cOTLdata[$nc]['char_data'][$i]['orig_type'];
5592 				}
5593 				$carac['group'] = $cOTLdata[$nc]['group'][$i];
5594 				$carac['chunkid'] = $chunkorder[$nc]; // gives font id and/or object ID
5595 
5596 				$maxlevel = max((isset($carac['level']) ? $carac['level'] : 0), $maxlevel);
5597 				$bidiData[] = $carac;
5598 			}
5599 		}
5600 		if ($maxlevel == 0) {
5601 			return;
5602 		}
5603 
5604 		$numchars = count($bidiData);
5605 
5606 		// L1. On each line, reset the embedding level of the following characters to the paragraph embedding level:
5607 		//  1. Segment separators (Tab) 'S',
5608 		//  2. Paragraph separators 'B',
5609 		//  3. Any sequence of whitespace characters 'WS' preceding a segment separator or paragraph separator, and
5610 		//  4. Any sequence of whitespace characters 'WS' at the end of the line.
5611 		//  The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2*******
5612 		//  Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line.
5613 		// Set the initial paragraph embedding level
5614 		if ($blockdir == 'rtl') {
5615 			$pel = 1;
5616 		} else {
5617 			$pel = 0;
5618 		}
5619 
5620 		for ($i = ($numchars - 1); $i > 0; $i--) {
5621 			if ($bidiData[$i]['type'] == Ucdn::BIDI_CLASS_WS || (isset($bidiData[$i]['orig_type']) && $bidiData[$i]['orig_type'] == Ucdn::BIDI_CLASS_WS)) {
5622 				$bidiData[$i]['level'] = $pel;
5623 			} else {
5624 				break;
5625 			}
5626 		}
5627 
5628 		// L2. From the highest level found in the text to the lowest odd level on each line, including intermediate levels not actually present in the text, reverse any contiguous sequence of characters that are at that level or higher.
5629 		for ($j = $maxlevel; $j > 0; $j--) {
5630 			$ordarray = [];
5631 			$revarr = [];
5632 			$onlevel = false;
5633 			for ($i = 0; $i < $numchars; ++$i) {
5634 				if ($bidiData[$i]['level'] >= $j) {
5635 					$onlevel = true;
5636 					// L4. A character is depicted by a mirrored glyph if and only if (a) the resolved directionality of that character is R, and (b) the Bidi_Mirrored property value of that character is true.
5637 					if (isset(Ucdn::$mirror_pairs[$bidiData[$i]['uni']]) && $bidiData[$i]['type'] == Ucdn::BIDI_CLASS_R) {
5638 						$bidiData[$i]['uni'] = Ucdn::$mirror_pairs[$bidiData[$i]['uni']];
5639 					}
5640 
5641 					$revarr[] = $bidiData[$i];
5642 				} else {
5643 					if ($onlevel) {
5644 						$revarr = array_reverse($revarr);
5645 						$ordarray = array_merge($ordarray, $revarr);
5646 						$revarr = [];
5647 						$onlevel = false;
5648 					}
5649 					$ordarray[] = $bidiData[$i];
5650 				}
5651 			}
5652 			if ($onlevel) {
5653 				$revarr = array_reverse($revarr);
5654 				$ordarray = array_merge($ordarray, $revarr);
5655 			}
5656 			$bidiData = $ordarray;
5657 		}
5658 
5659 		$content = [];
5660 		$cOTLdata = [];
5661 		$chunkorder = [];
5662 
5663 		$nc = -1; // New chunk order ID
5664 		$chunkid = -1;
5665 
5666 		foreach ($bidiData as $carac) {
5667 			if ($carac['chunkid'] != $chunkid) {
5668 				$nc++;
5669 				$chunkorder[$nc] = $carac['chunkid'];
5670 				$cctr = 0;
5671 				$content[$nc] = '';
5672 				$cOTLdata[$nc]['group'] = '';
5673 			}
5674 			if ($carac['uni'] != 0xFFFC) {   // Object replacement character (65532)
5675 				$content[$nc] .= UtfString::code2utf($carac['uni']);
5676 				$cOTLdata[$nc]['group'] .= $carac['group'];
5677 				if (!empty($carac['GPOSinfo'])) {
5678 					if (isset($carac['GPOSinfo'])) {
5679 						$cOTLdata[$nc]['GPOSinfo'][$cctr] = $carac['GPOSinfo'];
5680 					}
5681 					$cOTLdata[$nc]['GPOSinfo'][$cctr]['wDir'] = ($carac['level'] % 2) ? 'RTL' : 'LTR';
5682 				}
5683 			}
5684 			$chunkid = $carac['chunkid'];
5685 			$cctr++;
5686 		}
5687 	}
5688 
5689 	public function splitOTLdata(&$cOTLdata, $OTLcutoffpos, $OTLrestartpos = '')
5690 	{
5691 		if (!$OTLrestartpos) {
5692 			$OTLrestartpos = $OTLcutoffpos;
5693 		}
5694 		$newOTLdata = ['GPOSinfo' => [], 'char_data' => []];
5695 		$newOTLdata['group'] = substr($cOTLdata['group'], $OTLrestartpos);
5696 		$cOTLdata['group'] = substr($cOTLdata['group'], 0, $OTLcutoffpos);
5697 
5698 		if (isset($cOTLdata['GPOSinfo']) && $cOTLdata['GPOSinfo']) {
5699 			foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5700 				if ($k >= $OTLrestartpos) {
5701 					$newOTLdata['GPOSinfo'][($k - $OTLrestartpos)] = $val;
5702 				}
5703 				if ($k >= $OTLcutoffpos) {
5704 					unset($cOTLdata['GPOSinfo'][$k]);
5705 					//$cOTLdata['GPOSinfo'][$k] = array();
5706 				}
5707 			}
5708 		}
5709 		if (isset($cOTLdata['char_data'])) {
5710 			$newOTLdata['char_data'] = array_slice($cOTLdata['char_data'], $OTLrestartpos);
5711 			array_splice($cOTLdata['char_data'], $OTLcutoffpos);
5712 		}
5713 
5714 		// Not necessary - easier to debug
5715 		if (isset($cOTLdata['GPOSinfo'])) {
5716 			ksort($cOTLdata['GPOSinfo']);
5717 		}
5718 		if (isset($newOTLdata['GPOSinfo'])) {
5719 			ksort($newOTLdata['GPOSinfo']);
5720 		}
5721 
5722 		return $newOTLdata;
5723 	}
5724 
5725 	public function sliceOTLdata($OTLdata, $pos, $len)
5726 	{
5727 		$newOTLdata = ['GPOSinfo' => [], 'char_data' => []];
5728 		$newOTLdata['group'] = substr($OTLdata['group'], $pos, $len);
5729 
5730 		if ($OTLdata['GPOSinfo']) {
5731 			foreach ($OTLdata['GPOSinfo'] as $k => $val) {
5732 				if ($k >= $pos && $k < ($pos + $len)) {
5733 					$newOTLdata['GPOSinfo'][($k - $pos)] = $val;
5734 				}
5735 			}
5736 		}
5737 
5738 		if (isset($OTLdata['char_data'])) {
5739 			$newOTLdata['char_data'] = array_slice($OTLdata['char_data'], $pos, $len);
5740 		}
5741 
5742 		// Not necessary - easier to debug
5743 		if ($newOTLdata['GPOSinfo']) {
5744 			ksort($newOTLdata['GPOSinfo']);
5745 		}
5746 
5747 		return $newOTLdata;
5748 	}
5749 
5750 	/**
5751 	 * Remove one or more occurrences of $char (single character) from $txt and adjust OTLdata
5752 	 */
5753 	public function removeChar(&$txt, &$cOTLdata, $char)
5754 	{
5755 		while (mb_strpos($txt, $char, 0, $this->mpdf->mb_enc) !== false) {
5756 			$pos = mb_strpos($txt, $char, 0, $this->mpdf->mb_enc);
5757 			$newGPOSinfo = [];
5758 			$cOTLdata['group'] = substr_replace($cOTLdata['group'], '', $pos, 1);
5759 			if ($cOTLdata['GPOSinfo']) {
5760 				foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5761 					if ($k > $pos) {
5762 						$newGPOSinfo[($k - 1)] = $val;
5763 					} elseif ($k != $pos) {
5764 						$newGPOSinfo[$k] = $val;
5765 					}
5766 				}
5767 				$cOTLdata['GPOSinfo'] = $newGPOSinfo;
5768 			}
5769 			if (isset($cOTLdata['char_data'])) {
5770 				array_splice($cOTLdata['char_data'], $pos, 1);
5771 			}
5772 
5773 			$txt = preg_replace("/" . $char . "/", '', $txt, 1);
5774 		}
5775 	}
5776 
5777 	/**
5778 	 * Remove one or more occurrences of $char (single character) from $txt and adjust OTLdata
5779 	 */
5780 	public function replaceSpace(&$txt, &$cOTLdata)
5781 	{
5782 		$char = chr(194) . chr(160); // NBSP
5783 		while (mb_strpos($txt, $char, 0, $this->mpdf->mb_enc) !== false) {
5784 			$pos = mb_strpos($txt, $char, 0, $this->mpdf->mb_enc);
5785 			if ($cOTLdata['char_data'][$pos]['uni'] == 160) {
5786 				$cOTLdata['char_data'][$pos]['uni'] = 32;
5787 			}
5788 			$txt = preg_replace("/" . $char . "/", ' ', $txt, 1);
5789 		}
5790 	}
5791 
5792 	public function trimOTLdata(&$cOTLdata, $Left = true, $Right = true)
5793 	{
5794 		$len = (!is_array($cOTLdata) || $cOTLdata['char_data'] === null) ? 0 : count($cOTLdata['char_data']);
5795 		$nLeft = 0;
5796 		$nRight = 0;
5797 		for ($i = 0; $i < $len; $i++) {
5798 			if ($cOTLdata['char_data'][$i]['uni'] == 32 || $cOTLdata['char_data'][$i]['uni'] == 12288) {
5799 				$nLeft++;
5800 			} // 12288 = 0x3000 = CJK space
5801 			else {
5802 				break;
5803 			}
5804 		}
5805 		for ($i = ($len - 1); $i >= 0; $i--) {
5806 			if ($cOTLdata['char_data'][$i]['uni'] == 32 || $cOTLdata['char_data'][$i]['uni'] == 12288) {
5807 				$nRight++;
5808 			} // 12288 = 0x3000 = CJK space
5809 			else {
5810 				break;
5811 			}
5812 		}
5813 
5814 		// Trim Right
5815 		if ($Right && $nRight) {
5816 			$cOTLdata['group'] = substr($cOTLdata['group'], 0, strlen($cOTLdata['group']) - $nRight);
5817 			if ($cOTLdata['GPOSinfo']) {
5818 				foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5819 					if ($k >= $len - $nRight) {
5820 						unset($cOTLdata['GPOSinfo'][$k]);
5821 					}
5822 				}
5823 			}
5824 			if (isset($cOTLdata['char_data'])) {
5825 				for ($i = 0; $i < $nRight; $i++) {
5826 					array_pop($cOTLdata['char_data']);
5827 				}
5828 			}
5829 		}
5830 		// Trim Left
5831 		if ($Left && $nLeft) {
5832 			$cOTLdata['group'] = substr($cOTLdata['group'], $nLeft);
5833 			if ($cOTLdata['GPOSinfo']) {
5834 				$newPOSinfo = [];
5835 				foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5836 					if ($k >= $nLeft) {
5837 						$newPOSinfo[$k - $nLeft] = $cOTLdata['GPOSinfo'][$k];
5838 					}
5839 				}
5840 				$cOTLdata['GPOSinfo'] = $newPOSinfo;
5841 			}
5842 			if (isset($cOTLdata['char_data'])) {
5843 				for ($i = 0; $i < $nLeft; $i++) {
5844 					array_shift($cOTLdata['char_data']);
5845 				}
5846 			}
5847 		}
5848 	}
5849 
5850 	////////////////////////////////////////////////////////////////
5851 	//////////         GENERAL OTL FUNCTIONS       /////////////////
5852 	////////////////////////////////////////////////////////////////
5853 
5854 	private function glyphToChar($gid)
5855 	{
5856 		return (ord($this->glyphIDtoUni[$gid * 3]) << 16) + (ord($this->glyphIDtoUni[$gid * 3 + 1]) << 8) + ord($this->glyphIDtoUni[$gid * 3 + 2]);
5857 	}
5858 
5859 	private function unicode_hex($unicode_dec)
5860 	{
5861 		return (str_pad(strtoupper(dechex($unicode_dec)), 5, '0', STR_PAD_LEFT));
5862 	}
5863 
5864 	private function seek($pos)
5865 	{
5866 		$this->_pos = $pos;
5867 	}
5868 
5869 	private function skip($delta)
5870 	{
5871 		$this->_pos += $delta;
5872 	}
5873 
5874 	private function read_short()
5875 	{
5876 		$a = (ord($this->ttfOTLdata[$this->_pos]) << 8) + ord($this->ttfOTLdata[$this->_pos + 1]);
5877 		if ($a & (1 << 15)) {
5878 			$a = ($a - (1 << 16));
5879 		}
5880 		$this->_pos += 2;
5881 		return $a;
5882 	}
5883 
5884 	private function read_ushort()
5885 	{
5886 		$a = (ord($this->ttfOTLdata[$this->_pos]) << 8) + ord($this->ttfOTLdata[$this->_pos + 1]);
5887 		$this->_pos += 2;
5888 		return $a;
5889 	}
5890 
5891 	private function _getCoverageGID()
5892 	{
5893 		// Called from Lookup Type 1, Format 1 - returns glyphIDs rather than hexstrings
5894 		// Need to do this separately to cache separately
5895 		// Otherwise the same as fn below _getCoverage
5896 		$offset = $this->_pos;
5897 		if (isset($this->LuDataCache[$this->fontkey]['GID'][$offset])) {
5898 			$g = $this->LuDataCache[$this->fontkey]['GID'][$offset];
5899 		} else {
5900 			$g = [];
5901 			$CoverageFormat = $this->read_ushort();
5902 			if ($CoverageFormat == 1) {
5903 				$CoverageGlyphCount = $this->read_ushort();
5904 				for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) {
5905 					$glyphID = $this->read_ushort();
5906 					$g[] = $glyphID;
5907 				}
5908 			}
5909 			if ($CoverageFormat == 2) {
5910 				$RangeCount = $this->read_ushort();
5911 				for ($r = 0; $r < $RangeCount; $r++) {
5912 					$start = $this->read_ushort();
5913 					$end = $this->read_ushort();
5914 					$StartCoverageIndex = $this->read_ushort(); // n/a
5915 					for ($glyphID = $start; $glyphID <= $end; $glyphID++) {
5916 						$g[] = $glyphID;
5917 					}
5918 				}
5919 			}
5920 			$this->LuDataCache[$this->fontkey]['GID'][$offset] = $g;
5921 		}
5922 		return $g;
5923 	}
5924 
5925 	private function _getCoverage()
5926 	{
5927 		$offset = $this->_pos;
5928 		if (isset($this->LuDataCache[$this->fontkey][$offset])) {
5929 			$g = $this->LuDataCache[$this->fontkey][$offset];
5930 		} else {
5931 			$g = [];
5932 			$CoverageFormat = $this->read_ushort();
5933 			if ($CoverageFormat == 1) {
5934 				$CoverageGlyphCount = $this->read_ushort();
5935 				for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) {
5936 					$glyphID = $this->read_ushort();
5937 					$g[] = $this->unicode_hex($this->glyphToChar($glyphID));
5938 				}
5939 			}
5940 			if ($CoverageFormat == 2) {
5941 				$RangeCount = $this->read_ushort();
5942 				for ($r = 0; $r < $RangeCount; $r++) {
5943 					$start = $this->read_ushort();
5944 					$end = $this->read_ushort();
5945 					$StartCoverageIndex = $this->read_ushort(); // n/a
5946 					for ($glyphID = $start; $glyphID <= $end; $glyphID++) {
5947 						$g[] = $this->unicode_hex($this->glyphToChar($glyphID));
5948 					}
5949 				}
5950 			}
5951 			$this->LuDataCache[$this->fontkey][$offset] = $g;
5952 		}
5953 		return $g;
5954 	}
5955 
5956 	private function _getClasses($offset)
5957 	{
5958 		if (isset($this->LuDataCache[$this->fontkey][$offset])) {
5959 			$GlyphByClass = $this->LuDataCache[$this->fontkey][$offset];
5960 		} else {
5961 			$this->seek($offset);
5962 			$ClassFormat = $this->read_ushort();
5963 			$GlyphByClass = [];
5964 			if ($ClassFormat == 1) {
5965 				$StartGlyph = $this->read_ushort();
5966 				$GlyphCount = $this->read_ushort();
5967 				for ($i = 0; $i < $GlyphCount; $i++) {
5968 					$startGlyphID = $StartGlyph + $i;
5969 					$endGlyphID = $StartGlyph + $i;
5970 					$class = $this->read_ushort();
5971 					// Note: Font FreeSerif , tag "blws"
5972 					// $BacktrackClasses[0] is defined ? a mistake in the font ???
5973 					// Let's ignore for now
5974 					if ($class > 0) {
5975 						for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
5976 							if ($this->glyphToChar($g)) {
5977 								$GlyphByClass[$class][$this->glyphToChar($g)] = 1;
5978 							}
5979 						}
5980 					}
5981 				}
5982 			} elseif ($ClassFormat == 2) {
5983 				$tableCount = $this->read_ushort();
5984 				for ($i = 0; $i < $tableCount; $i++) {
5985 					$startGlyphID = $this->read_ushort();
5986 					$endGlyphID = $this->read_ushort();
5987 					$class = $this->read_ushort();
5988 					// Note: Font FreeSerif , tag "blws"
5989 					// $BacktrackClasses[0] is defined ? a mistake in the font ???
5990 					// Let's ignore for now
5991 					if ($class > 0) {
5992 						for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
5993 							if ($this->glyphToChar($g)) {
5994 								$GlyphByClass[$class][$this->glyphToChar($g)] = 1;
5995 							}
5996 						}
5997 					}
5998 				}
5999 			}
6000 			$this->LuDataCache[$this->fontkey][$offset] = $GlyphByClass;
6001 		}
6002 		return $GlyphByClass;
6003 	}
6004 
6005 	private function _getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $shaper, $useOTL, $mode)
6006 	{
6007 		// ScriptLang is the array of available script/lang tags supported by the font
6008 		// $scriptblock is the (number/code) for the script of the actual text string based on Unicode properties (Ucdn::$uni_scriptblock)
6009 		// $scripttag is the default tag derived from $scriptblock
6010 		/*
6011 		  http://www.microsoft.com/typography/otspec/ttoreg.htm
6012 		  http://www.microsoft.com/typography/otspec/scripttags.htm
6013 
6014 		  Values for useOTL
6015 
6016 		  Bit   dn  hn  Value
6017 		  1 1   0x0001  GSUB/GPOS - Latin scripts
6018 		  2 2   0x0002  GSUB/GPOS - Cyrillic scripts
6019 		  3 4   0x0004  GSUB/GPOS - Greek scripts
6020 		  4 8   0x0008  GSUB/GPOS - CJK scripts (excluding Hangul-Jamo)
6021 		  5 16  0x0010  (Reserved)
6022 		  6 32  0x0020  (Reserved)
6023 		  7 64  0x0040  (Reserved)
6024 		  8 128 0x0080  GSUB/GPOS - All other scripts (including all RTL scripts, complex scripts with shapers etc)
6025 
6026 		  NB If change for RTL - cf. function magic_reverse_dir in mpdf.php to update
6027 
6028 		 */
6029 
6030 
6031 		if ($scriptblock == Ucdn::SCRIPT_LATIN) {
6032 			if (!($useOTL & 0x01)) {
6033 				return ['', false];
6034 			}
6035 		} elseif ($scriptblock == Ucdn::SCRIPT_CYRILLIC) {
6036 			if (!($useOTL & 0x02)) {
6037 				return ['', false];
6038 			}
6039 		} elseif ($scriptblock == Ucdn::SCRIPT_GREEK) {
6040 			if (!($useOTL & 0x04)) {
6041 				return ['', false];
6042 			}
6043 		} elseif ($scriptblock >= Ucdn::SCRIPT_HIRAGANA && $scriptblock <= Ucdn::SCRIPT_YI) {
6044 			if (!($useOTL & 0x08)) {
6045 				return ['', false];
6046 			}
6047 		} else {
6048 			if (!($useOTL & 0x80)) {
6049 				return ['', false];
6050 			}
6051 		}
6052 
6053 		//  If availabletags includes scripttag - choose
6054 		if (isset($ScriptLang[$scripttag])) {
6055 			return [$scripttag, false];
6056 		}
6057 
6058 		//  If INDIC (or Myanmar) and available tag not includes new version, check if includes old version & choose old version
6059 		if ($shaper) {
6060 			switch ($scripttag) {
6061 				case 'bng2':
6062 					if (isset($ScriptLang['beng'])) {
6063 						return ['beng', true];
6064 					}
6065 					// fallthrough
6066 				case 'dev2':
6067 					if (isset($ScriptLang['deva'])) {
6068 						return ['deva', true];
6069 					}
6070 					// fallthrough
6071 				case 'gjr2':
6072 					if (isset($ScriptLang['gujr'])) {
6073 						return ['gujr', true];
6074 					}
6075 					// fallthrough
6076 				case 'gur2':
6077 					if (isset($ScriptLang['guru'])) {
6078 						return ['guru', true];
6079 					}
6080 					// fallthrough
6081 				case 'knd2':
6082 					if (isset($ScriptLang['knda'])) {
6083 						return ['knda', true];
6084 					}
6085 					// fallthrough
6086 				case 'mlm2':
6087 					if (isset($ScriptLang['mlym'])) {
6088 						return ['mlym', true];
6089 					}
6090 					// fallthrough
6091 				case 'ory2':
6092 					if (isset($ScriptLang['orya'])) {
6093 						return ['orya', true];
6094 					}
6095 					// fallthrough
6096 				case 'tml2':
6097 					if (isset($ScriptLang['taml'])) {
6098 						return ['taml', true];
6099 					}
6100 					// fallthrough
6101 				case 'tel2':
6102 					if (isset($ScriptLang['telu'])) {
6103 						return ['telu', true];
6104 					}
6105 					// fallthrough
6106 				case 'mym2':
6107 					if (isset($ScriptLang['mymr'])) {
6108 						return ['mymr', true];
6109 					}
6110 			}
6111 		}
6112 
6113 		//  choose DFLT if present
6114 		if (isset($ScriptLang['DFLT'])) {
6115 			return ['DFLT', false];
6116 		}
6117 		//  else choose dflt if present
6118 		if (isset($ScriptLang['dflt'])) {
6119 			return ['dflt', false];
6120 		}
6121 		//  else return no scriptTag
6122 		if (isset($ScriptLang['latn'])) {
6123 			return ['latn', false];
6124 		}
6125 		//  else return no scriptTag
6126 		return ['', false];
6127 	}
6128 
6129 	// LangSys tags
6130 	private function _getOTLLangTag($ietf, $available)
6131 	{
6132 		// http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
6133 		// http://www.microsoft.com/typography/otspec/languagetags.htm
6134 		// IETF tag = e.g. en-US, und-Arab, sr-Cyrl cf. class LangToFont
6135 		if ($available == '') {
6136 			return '';
6137 		}
6138 
6139 		$tags = $ietf
6140 			? preg_split('/-/', $ietf)
6141 			: [];
6142 
6143 		$lang = '';
6144 		$country = '';
6145 		$script = '';
6146 
6147 		$lang = isset($tags[0])
6148 			? strtolower($tags[0])
6149 			: '';
6150 
6151 		if (isset($tags[1]) && $tags[1]) {
6152 			if (strlen($tags[1]) == 2) {
6153 				$country = strtolower($tags[1]);
6154 			}
6155 		}
6156 
6157 		if (isset($tags[2]) && $tags[2]) {
6158 			$country = strtolower($tags[2]);
6159 		}
6160 
6161 		if ($lang != '' && isset(Ucdn::$ot_languages[$lang])) {
6162 			$langsys = Ucdn::$ot_languages[$lang];
6163 		} elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$lang . '' . $country])) {
6164 			$langsys = Ucdn::$ot_languages[$lang . '' . $country];
6165 		} else {
6166 			$langsys = "DFLT";
6167 		}
6168 
6169 		if (strpos($available, $langsys) === false) {
6170 			if (strpos($available, "DFLT") !== false) {
6171 				return "DFLT";
6172 			} else {
6173 				return '';
6174 			}
6175 		}
6176 
6177 		return $langsys;
6178 	}
6179 
6180 	private function _dumpproc($GPOSSUB, $lookupID, $subtable, $Type, $Format, $ptr, $currGlyph, $level)
6181 	{
6182 		echo '<div style="padding-left: ' . ($level * 2) . 'em;">';
6183 		echo $GPOSSUB . ' LookupID #' . $lookupID . ' Subtable#' . $subtable . ' Type: ' . $Type . ' Format: ' . $Format . '<br />';
6184 		echo '<div style="font-family:monospace">';
6185 		echo 'Glyph position: ' . $ptr . ' Current Glyph: ' . $currGlyph . '<br />';
6186 
6187 		for ($i = 0; $i < count($this->OTLdata); $i++) {
6188 			if ($i == $ptr) {
6189 				echo '<b>';
6190 			}
6191 			echo $this->OTLdata[$i]['hex'] . ' ';
6192 			if ($i == $ptr) {
6193 				echo '</b>';
6194 			}
6195 		}
6196 		echo '<br />';
6197 
6198 		for ($i = 0; $i < count($this->OTLdata); $i++) {
6199 			if ($i == $ptr) {
6200 				echo '<b>';
6201 			}
6202 			echo str_pad($this->OTLdata[$i]['uni'], 5) . ' ';
6203 			if ($i == $ptr) {
6204 				echo '</b>';
6205 			}
6206 		}
6207 		echo '<br />';
6208 
6209 		if ($GPOSSUB == 'GPOS') {
6210 			for ($i = 0; $i < count($this->OTLdata); $i++) {
6211 				if (!empty($this->OTLdata[$i]['GPOSinfo'])) {
6212 					echo $this->OTLdata[$i]['hex'] . ' &#x' . $this->OTLdata[$i]['hex'] . '; ';
6213 					print_r($this->OTLdata[$i]['GPOSinfo']);
6214 					echo ' ';
6215 				}
6216 			}
6217 		}
6218 
6219 		echo '</div>';
6220 		echo '</div>';
6221 	}
6222 }
6223