xref: /plugin/dw2pdf/vendor/mpdf/mpdf/src/Otl.php (revision a7922d032219102b786ac11f636f95e6f52f7347)
1<?php
2
3namespace Mpdf;
4
5use Mpdf\Css\TextVars;
6use Mpdf\Fonts\FontCache;
7use Mpdf\Shaper\Indic;
8use Mpdf\Shaper\Myanmar;
9use Mpdf\Shaper\Sea;
10
11class Otl
12{
13
14	const _OTL_OLD_SPEC_COMPAT_1 = true;
15	const _DICT_NODE_TYPE_SPLIT = 0x01;
16	const _DICT_NODE_TYPE_LINEAR = 0x02;
17	const _DICT_INTERMEDIATE_MATCH = 0x03;
18	const _DICT_FINAL_MATCH = 0x04;
19
20	private $mpdf;
21
22	private $fontCache;
23
24	var $arabLeftJoining;
25
26	var $arabRightJoining;
27
28	var $arabTransparentJoin;
29
30	var $arabTransparent;
31
32	var $GSUBdata;
33
34	var $GPOSdata;
35
36	var $GSUBfont;
37
38	var $fontkey;
39
40	var $ttfOTLdata;
41
42	var $glyphIDtoUni;
43
44	var $_pos;
45
46	var $GSUB_offset;
47
48	var $GPOS_offset;
49
50	var $MarkAttachmentType;
51
52	var $MarkGlyphSets;
53
54	var $GlyphClassMarks;
55
56	var $GlyphClassLigatures;
57
58	var $GlyphClassBases;
59
60	var $GlyphClassComponents;
61
62	var $Ignores;
63
64	var $LuCoverage;
65
66	var $OTLdata;
67
68	var $assocLigs;
69
70	var $assocMarks;
71
72	var $shaper;
73
74	var $restrictToSyllable;
75
76	var $lbdicts; // Line-breaking dictionaries
77
78	var $LuDataCache;
79
80	var $arabGlyphs;
81
82	var $current_fh;
83
84	var $Entry;
85
86	var $Exit;
87
88	var $GDEFdata;
89
90	var $GPOSLookups;
91
92	var $GSLuCoverage;
93
94	var $GSUB_length;
95
96	var $GSUBLookups;
97
98	var $schOTLdata;
99
100	var $debugOTL = false;
101
102	public function __construct(Mpdf $mpdf, FontCache $fontCache)
103	{
104		$this->mpdf = $mpdf;
105		$this->fontCache = $fontCache;
106
107		$this->current_fh = '';
108
109		$this->lbdicts = [];
110		$this->LuDataCache = [];
111	}
112
113	function applyOTL($str, $useOTL)
114	{
115		if (!$this->arabLeftJoining) {
116			$this->arabic_initialise();
117		}
118
119		$this->OTLdata = [];
120		if (trim($str) == '') {
121			return $str;
122		}
123		if (!$useOTL) {
124			return $str;
125		}
126
127		// 1. Load GDEF data
128		//==============================
129		$this->fontkey = $this->mpdf->CurrentFont['fontkey'];
130		$this->glyphIDtoUni = $this->mpdf->CurrentFont['glyphIDtoUni'];
131		if (!isset($this->GDEFdata[$this->fontkey])) {
132			include $this->fontCache->tempFilename($this->fontkey . '.GDEFdata.php');
133			$this->GSUB_offset = $this->GDEFdata[$this->fontkey]['GSUB_offset'] = $GSUB_offset;
134			$this->GPOS_offset = $this->GDEFdata[$this->fontkey]['GPOS_offset'] = $GPOS_offset;
135			$this->GSUB_length = $this->GDEFdata[$this->fontkey]['GSUB_length'] = $GSUB_length;
136			$this->MarkAttachmentType = $this->GDEFdata[$this->fontkey]['MarkAttachmentType'] = $MarkAttachmentType;
137			$this->MarkGlyphSets = $this->GDEFdata[$this->fontkey]['MarkGlyphSets'] = $MarkGlyphSets;
138			$this->GlyphClassMarks = $this->GDEFdata[$this->fontkey]['GlyphClassMarks'] = $GlyphClassMarks;
139			$this->GlyphClassLigatures = $this->GDEFdata[$this->fontkey]['GlyphClassLigatures'] = $GlyphClassLigatures;
140			$this->GlyphClassComponents = $this->GDEFdata[$this->fontkey]['GlyphClassComponents'] = $GlyphClassComponents;
141			$this->GlyphClassBases = $this->GDEFdata[$this->fontkey]['GlyphClassBases'] = $GlyphClassBases;
142		} else {
143			$this->GSUB_offset = $this->GDEFdata[$this->fontkey]['GSUB_offset'];
144			$this->GPOS_offset = $this->GDEFdata[$this->fontkey]['GPOS_offset'];
145			$this->GSUB_length = $this->GDEFdata[$this->fontkey]['GSUB_length'];
146			$this->MarkAttachmentType = $this->GDEFdata[$this->fontkey]['MarkAttachmentType'];
147			$this->MarkGlyphSets = $this->GDEFdata[$this->fontkey]['MarkGlyphSets'];
148			$this->GlyphClassMarks = $this->GDEFdata[$this->fontkey]['GlyphClassMarks'];
149			$this->GlyphClassLigatures = $this->GDEFdata[$this->fontkey]['GlyphClassLigatures'];
150			$this->GlyphClassComponents = $this->GDEFdata[$this->fontkey]['GlyphClassComponents'];
151			$this->GlyphClassBases = $this->GDEFdata[$this->fontkey]['GlyphClassBases'];
152		}
153
154		// 2. Prepare string as HEX string and Analyse character properties
155		//=================================================================
156		$earr = $this->mpdf->UTF8StringToArray($str, false);
157
158		$scriptblock = 0;
159		$scriptblocks = [];
160		$scriptblocks[0] = 0;
161		$vstr = '';
162		$OTLdata = [];
163		$subchunk = 0;
164		$charctr = 0;
165		foreach ($earr as $char) {
166			$ucd_record = Ucdn::get_ucd_record($char);
167			$sbl = $ucd_record[6];
168
169			// Special case - Arabic End of Ayah
170			if ($char == 1757) {
171				$sbl = Ucdn::SCRIPT_ARABIC;
172			}
173
174			if ($sbl && $sbl != 40 && $sbl != 102) {
175				if ($scriptblock == 0) {
176					$scriptblock = $sbl;
177					$scriptblocks[$subchunk] = $scriptblock;
178				} elseif ($scriptblock > 0 && $scriptblock != $sbl) {
179					// *************************************************
180					// NEW (non-common) Script encountered in this chunk. Start a new subchunk
181					$subchunk++;
182					$scriptblock = $sbl;
183					$charctr = 0;
184					$scriptblocks[$subchunk] = $scriptblock;
185				}
186			}
187
188			$OTLdata[$subchunk][$charctr]['general_category'] = $ucd_record[0];
189			$OTLdata[$subchunk][$charctr]['bidi_type'] = $ucd_record[2];
190
191			//$OTLdata[$subchunk][$charctr]['combining_class'] = $ucd_record[1];
192			//$OTLdata[$subchunk][$charctr]['bidi_type'] = $ucd_record[2];
193			//$OTLdata[$subchunk][$charctr]['mirrored'] = $ucd_record[3];
194			//$OTLdata[$subchunk][$charctr]['east_asian_width'] = $ucd_record[4];
195			//$OTLdata[$subchunk][$charctr]['normalization_check'] = $ucd_record[5];
196			//$OTLdata[$subchunk][$charctr]['script'] = $ucd_record[6];
197
198			$charasstr = $this->unicode_hex($char);
199
200			if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
201				$OTLdata[$subchunk][$charctr]['group'] = 'M';
202			} elseif ($char == 32 || $char == 12288) {
203				$OTLdata[$subchunk][$charctr]['group'] = 'S';
204			} // 12288 = 0x3000 = CJK space
205			else {
206				$OTLdata[$subchunk][$charctr]['group'] = 'C';
207			}
208
209			$OTLdata[$subchunk][$charctr]['uni'] = $char;
210			$OTLdata[$subchunk][$charctr]['hex'] = $charasstr;
211			$charctr++;
212		}
213
214		/* PROCESS EACH SUBCHUNK WITH DIFFERENT SCRIPTS */
215		for ($sch = 0; $sch <= $subchunk; $sch++) {
216			$this->OTLdata = $OTLdata[$sch];
217			$scriptblock = $scriptblocks[$sch];
218
219			// 3. Get Appropriate Scripts, and Shaper engine from analysing text and list of available scripts/langsys in font
220			//==============================
221			// Based on actual script block of text, select shaper (and line-breaking dictionaries)
222			if (Ucdn::SCRIPT_DEVANAGARI <= $scriptblock && $scriptblock <= Ucdn::SCRIPT_MALAYALAM) {
223				$this->shaper = "I";
224			} // INDIC shaper
225			elseif ($scriptblock == Ucdn::SCRIPT_ARABIC || $scriptblock == Ucdn::SCRIPT_SYRIAC) {
226				$this->shaper = "A";
227			} // ARABIC shaper
228			elseif ($scriptblock == Ucdn::SCRIPT_NKO || $scriptblock == Ucdn::SCRIPT_MANDAIC) {
229				$this->shaper = "A";
230			} // ARABIC shaper
231			elseif ($scriptblock == Ucdn::SCRIPT_KHMER) {
232				$this->shaper = "K";
233			} // KHMER shaper
234			elseif ($scriptblock == Ucdn::SCRIPT_THAI) {
235				$this->shaper = "T";
236			} // THAI shaper
237			elseif ($scriptblock == Ucdn::SCRIPT_LAO) {
238				$this->shaper = "L";
239			} // LAO shaper
240			elseif ($scriptblock == Ucdn::SCRIPT_SINHALA) {
241				$this->shaper = "S";
242			} // SINHALA shaper
243			elseif ($scriptblock == Ucdn::SCRIPT_MYANMAR) {
244				$this->shaper = "M";
245			} // MYANMAR shaper
246			elseif ($scriptblock == Ucdn::SCRIPT_NEW_TAI_LUE) {
247				$this->shaper = "E";
248			} // SEA South East Asian shaper
249			elseif ($scriptblock == Ucdn::SCRIPT_CHAM) {
250				$this->shaper = "E";
251			} // SEA South East Asian shaper
252			elseif ($scriptblock == Ucdn::SCRIPT_TAI_THAM) {
253				$this->shaper = "E";
254			} // SEA South East Asian shaper
255			else {
256				$this->shaper = "";
257			}
258			// Get scripttag based on actual text script
259			$scripttag = Ucdn::$uni_scriptblock[$scriptblock];
260
261			$GSUBscriptTag = '';
262			$GSUBlangsys = '';
263			$GPOSscriptTag = '';
264			$GPOSlangsys = '';
265			$is_old_spec = false;
266
267			$ScriptLang = $this->mpdf->CurrentFont['GSUBScriptLang'];
268			if (count($ScriptLang)) {
269				list($GSUBscriptTag, $is_old_spec) = $this->_getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $this->shaper, $useOTL, 'GSUB');
270				if ($this->mpdf->fontLanguageOverride && strpos($ScriptLang[$GSUBscriptTag], $this->mpdf->fontLanguageOverride) !== false) {
271					$GSUBlangsys = str_pad($this->mpdf->fontLanguageOverride, 4);
272				} elseif ($GSUBscriptTag && isset($ScriptLang[$GSUBscriptTag]) && $ScriptLang[$GSUBscriptTag] != '') {
273					$GSUBlangsys = $this->_getOTLLangTag($this->mpdf->currentLang, $ScriptLang[$GSUBscriptTag]);
274				}
275			}
276			$ScriptLang = $this->mpdf->CurrentFont['GPOSScriptLang'];
277
278			// NB If after GSUB, the same script/lang exist for GPOS, just use these...
279			if ($GSUBscriptTag && $GSUBlangsys && isset($ScriptLang[$GSUBscriptTag]) && strpos($ScriptLang[$GSUBscriptTag], $GSUBlangsys) !== false) {
280				$GPOSlangsys = $GSUBlangsys;
281				$GPOSscriptTag = $GSUBscriptTag;
282			} // else repeat for GPOS
283			// [Font XBRiyaz has GSUB tables for latn, but not GPOS for latn]
284			elseif (count($ScriptLang)) {
285				list($GPOSscriptTag, $dummy) = $this->_getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $this->shaper, $useOTL, 'GPOS');
286				if ($GPOSscriptTag && $this->mpdf->fontLanguageOverride && strpos($ScriptLang[$GPOSscriptTag], $this->mpdf->fontLanguageOverride) !== false) {
287					$GPOSlangsys = str_pad($this->mpdf->fontLanguageOverride, 4);
288				} elseif ($GPOSscriptTag && isset($ScriptLang[$GPOSscriptTag]) && $ScriptLang[$GPOSscriptTag] != '') {
289					$GPOSlangsys = $this->_getOTLLangTag($this->mpdf->currentLang, $ScriptLang[$GPOSscriptTag]);
290				}
291			}
292
293			////////////////////////////////////////////////////////////////
294			// This is just for the font_dump_OTL utility to set script and langsys override
295			if (isset($this->mpdf->overrideOTLsettings) && isset($this->mpdf->overrideOTLsettings[$this->fontkey])) {
296				$GSUBscriptTag = $GPOSscriptTag = $this->mpdf->overrideOTLsettings[$this->fontkey]['script'];
297				$GSUBlangsys = $GPOSlangsys = $this->mpdf->overrideOTLsettings[$this->fontkey]['lang'];
298			}
299			////////////////////////////////////////////////////////////////
300
301			if (!$GSUBscriptTag && !$GSUBlangsys && !$GPOSscriptTag && !$GPOSlangsys) {
302				// Remove ZWJ and ZWNJ
303				for ($i = 0; $i < count($this->OTLdata); $i++) {
304					if ($this->OTLdata[$i]['uni'] == 8204 || $this->OTLdata[$i]['uni'] == 8205) {
305						array_splice($this->OTLdata, $i, 1);
306					}
307				}
308				$this->schOTLdata[$sch] = $this->OTLdata;
309				$this->OTLdata = [];
310				continue;
311			}
312
313			// Don't use MYANMAR shaper unless using v2 scripttag
314			if ($this->shaper == 'M' && $GSUBscriptTag != 'mym2') {
315				$this->shaper = '';
316			}
317
318			$GSUBFeatures = (isset($this->mpdf->CurrentFont['GSUBFeatures'][$GSUBscriptTag][$GSUBlangsys]) ? $this->mpdf->CurrentFont['GSUBFeatures'][$GSUBscriptTag][$GSUBlangsys] : false);
319			$GPOSFeatures = (isset($this->mpdf->CurrentFont['GPOSFeatures'][$GPOSscriptTag][$GPOSlangsys]) ? $this->mpdf->CurrentFont['GPOSFeatures'][$GPOSscriptTag][$GPOSlangsys] : false);
320
321			$this->assocLigs = []; // Ligatures[$posarr lpos] => nc
322			$this->assocMarks = [];  // assocMarks[$posarr mpos] => array(compID, ligPos)
323
324			if (!isset($this->GDEFdata[$this->fontkey]['GSUBGPOStables'])) {
325				$this->ttfOTLdata = $this->GDEFdata[$this->fontkey]['GSUBGPOStables'] = $this->fontCache->load($this->fontkey . '.GSUBGPOStables.dat', 'rb');
326				if (!$this->ttfOTLdata) {
327					throw new \Mpdf\MpdfException('Can\'t open file ' . $this->fontCache->tempFilename($this->fontkey . '.GSUBGPOStables.dat'));
328				}
329			} else {
330				$this->ttfOTLdata = $this->GDEFdata[$this->fontkey]['GSUBGPOStables'];
331			}
332
333			if ($this->debugOTL) {
334				$this->_dumpproc('BEGIN', '-', '-', '-', '-', -1, '-', 0);
335			}
336
337			////////////////////////////////////////////////////////////////
338			/////////  LINE BREAKING FOR KHMER, THAI + LAO /////////////////
339			////////////////////////////////////////////////////////////////
340			// Insert U+200B at word boundaries using dictionaries
341			if ($this->mpdf->useDictionaryLBR && ($this->shaper == "K" || $this->shaper == "T" || $this->shaper == "L")) {
342				// Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
343				$this->seaLineBreaking();
344			} // Insert U+200B at word boundaries for Tibetan
345			elseif ($this->mpdf->useTibetanLBR && $scriptblock == Ucdn::SCRIPT_TIBETAN) {
346				// Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
347				$this->tibetanLineBreaking();
348			}
349
350
351			////////////////////////////////////////////////////////////////
352			//////////       GSUB          /////////////////////////////////
353			////////////////////////////////////////////////////////////////
354			if (($useOTL & 0xFF) && $GSUBscriptTag && $GSUBlangsys && $GSUBFeatures) {
355				// 4. Load GSUB data, Coverage & Lookups
356				//=================================================================
357
358				$this->GSUBfont = $this->fontkey . '.GSUB.' . $GSUBscriptTag . '.' . $GSUBlangsys;
359
360				if (!isset($this->GSUBdata[$this->GSUBfont])) {
361					if ($this->fontCache->has($this->mpdf->CurrentFont['fontkey'] . '.GSUB.' . $GSUBscriptTag . '.' . $GSUBlangsys . '.php')) {
362						include $this->fontCache->tempFilename($this->mpdf->CurrentFont['fontkey'] . '.GSUB.' . $GSUBscriptTag . '.' . $GSUBlangsys . '.php');
363						$this->GSUBdata[$this->GSUBfont]['rtlSUB'] = $rtlSUB;
364						$this->GSUBdata[$this->GSUBfont]['finals'] = $finals;
365						if ($this->shaper == 'I') {
366							$this->GSUBdata[$this->GSUBfont]['rphf'] = $rphf;
367							$this->GSUBdata[$this->GSUBfont]['half'] = $half;
368							$this->GSUBdata[$this->GSUBfont]['pref'] = $pref;
369							$this->GSUBdata[$this->GSUBfont]['blwf'] = $blwf;
370							$this->GSUBdata[$this->GSUBfont]['pstf'] = $pstf;
371						}
372					} else {
373						$this->GSUBdata[$this->GSUBfont] = ['rtlSUB' => [], 'rphf' => [], 'rphf' => [],
374							'pref' => [], 'blwf' => [], 'pstf' => [], 'finals' => ''
375						];
376					}
377				}
378
379				if (!isset($this->GSUBdata[$this->fontkey])) {
380					include $this->fontCache->tempFilename($this->fontkey . '.GSUBdata.php');
381					$this->GSLuCoverage = $this->GSUBdata[$this->fontkey]['GSLuCoverage'] = $GSLuCoverage;
382				} else {
383					$this->GSLuCoverage = $this->GSUBdata[$this->fontkey]['GSLuCoverage'];
384				}
385
386				$this->GSUBLookups = $this->mpdf->CurrentFont['GSUBLookups'];
387
388
389				// 5(A). GSUB - Shaper - ARABIC
390				//==============================
391				if ($this->shaper == 'A') {
392					//-----------------------------------------------------------------------------------
393					// a. Apply initial GSUB Lookups (in order specified in lookup list but only selecting from certain tags)
394					//-----------------------------------------------------------------------------------
395					$tags = 'locl ccmp';
396					$omittags = '';
397					$usetags = $tags;
398					if (!empty($this->mpdf->OTLtags)) {
399						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, true);
400					}
401					$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
402
403					//-----------------------------------------------------------------------------------
404					// b. Apply context-specific forms GSUB Lookups (initial, isolated, medial, final)
405					//-----------------------------------------------------------------------------------
406					// Arab and Syriac are the only scripts requiring the special joining - which takes the place of
407					// isol fina medi init rules in GSUB (+ fin2 fin3 med2 in Syriac syrc)
408					$tags = 'isol fina fin2 fin3 medi med2 init';
409					$omittags = '';
410					$usetags = $tags;
411					if (!empty($this->mpdf->OTLtags)) {
412						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, true);
413					}
414
415					$this->arabGlyphs = $this->GSUBdata[$this->GSUBfont]['rtlSUB'];
416
417					$gcms = explode("| ", $this->GlyphClassMarks);
418					$gcm = [];
419					foreach ($gcms as $g) {
420						$gcm[hexdec($g)] = 1;
421					}
422					$this->arabTransparentJoin = $this->arabTransparent + $gcm;
423					$this->arabic_shaper($usetags, $GSUBscriptTag);
424
425					//-----------------------------------------------------------------------------------
426					// c. Set Kashida points (after joining occurred - medi, fina, init) but before other substitutions
427					//-----------------------------------------------------------------------------------
428					//if ($scriptblock == Ucdn::SCRIPT_ARABIC ) {
429					for ($i = 0; $i < count($this->OTLdata); $i++) {
430						// Put the kashida marker on the character BEFORE which is inserted the kashida
431						// Kashida marker is inverse of priority i.e. Priority 1 => 7, Priority 7 => 1.
432						// Priority 1   User-inserted Kashida 0640 = Tatweel
433						// The user entered a Kashida in a position
434						// Position: Before the user-inserted kashida
435						if ($this->OTLdata[$i]['uni'] == 0x0640) {
436							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 8; // Put before the next character
437						} // Priority 2   Seen (0633)  FEB3, FEB4; Sad (0635)  FEBB, FEBC
438						// Initial or medial form
439						// Connecting to the next character
440						// Position: After the character
441						elseif ($this->OTLdata[$i]['uni'] == 0xFEB3 || $this->OTLdata[$i]['uni'] == 0xFEB4 || $this->OTLdata[$i]['uni'] == 0xFEBB || $this->OTLdata[$i]['uni'] == 0xFEBC) {
442							$checkpos = $i + 1;
443							while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
444								$checkpos++;
445							}
446							if (isset($this->OTLdata[$checkpos])) {
447								$this->OTLdata[$checkpos]['GPOSinfo']['kashida'] = 7; // Put after marks on next character
448							}
449						} // Priority 3   Taa Marbutah (0629) FE94; Haa (062D) FEA2; Dal (062F) FEAA
450						// Final form
451						// Connecting to previous character
452						// Position: Before the character
453						elseif ($this->OTLdata[$i]['uni'] == 0xFE94 || $this->OTLdata[$i]['uni'] == 0xFEA2 || $this->OTLdata[$i]['uni'] == 0xFEAA) {
454							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 6;
455						} // Priority 4   Alef (0627) FE8E; Tah (0637) FEC2; Lam (0644) FEDE; Kaf (0643)  FEDA; Gaf (06AF) FB93
456						// Final form
457						// Connecting to previous character
458						// Position: Before the character
459						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) {
460							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 5;
461						} // Priority 5   RA (0631) FEAE; Ya (064A)  FEF2 FEF4; Alef Maqsurah (0649) FEF0 FBE9
462						// Final or Medial form
463						// Connected to preceding medial BAA (0628) = FE92
464						// Position: Before preceding medial Baa
465						// Although not mentioned in spec, added Farsi Yeh (06CC) FBFD FBFF; equivalent to 064A or 0649
466						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
467						) {
468							$checkpos = $i - 1;
469							while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
470								$checkpos--;
471							}
472							if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == 0xFE92) {
473								$this->OTLdata[$checkpos]['GPOSinfo']['kashida'] = 4; // ******* Before preceding BAA
474							}
475						} // Priority 6   WAW (0648) FEEE; Ain (0639) FECA; Qaf (0642) FED6; Fa (0641) FED2
476						// Final form
477						// Connecting to previous character
478						// Position: Before the character
479						elseif ($this->OTLdata[$i]['uni'] == 0xFEEE || $this->OTLdata[$i]['uni'] == 0xFECA || $this->OTLdata[$i]['uni'] == 0xFED6 || $this->OTLdata[$i]['uni'] == 0xFED2) {
480							$this->OTLdata[$i]['GPOSinfo']['kashida'] = 3;
481						}
482
483						// Priority 7   Other connecting characters
484						// Final form
485						// Connecting to previous character
486						// Position: Before the character
487						/* This isn't in the spec, but using MS WORD as a basis, give a lower priority to the 3 characters already checked
488						  in (5) above. Test case:
489						  &#x62e;&#x652;&#x631;&#x64e;&#x649;&#x670;
490						  &#x641;&#x64e;&#x62a;&#x64f;&#x630;&#x64e;&#x643;&#x651;&#x650;&#x631;
491						 */
492
493						if (!isset($this->OTLdata[$i]['GPOSinfo']['kashida'])) {
494							if (strpos($this->GSUBdata[$this->GSUBfont]['finals'], $this->OTLdata[$i]['hex']) !== false) { // ANY OTHER FINAL FORM
495								$this->OTLdata[$i]['GPOSinfo']['kashida'] = 2;
496							} elseif (strpos('0FEAE 0FEF0 0FEF2', $this->OTLdata[$i]['hex']) !== false) { // not already included in 5 above
497								$this->OTLdata[$i]['GPOSinfo']['kashida'] = 1;
498							}
499						}
500					}
501
502					//-----------------------------------------------------------------------------------
503					// d. Apply Presentation Forms GSUB Lookups (+ any discretionary) - Apply one at a time in Feature order
504					//-----------------------------------------------------------------------------------
505					$tags = 'rlig calt liga clig mset';
506
507					$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';
508					$usetags = $tags;
509					if (!empty($this->mpdf->OTLtags)) {
510						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
511					}
512
513					$ts = explode(' ', $usetags);
514					foreach ($ts as $ut) { //  - Apply one at a time in Feature order
515						$this->_applyGSUBrules($ut, $GSUBscriptTag, $GSUBlangsys);
516					}
517					//-----------------------------------------------------------------------------------
518					// e. NOT IN SPEC
519					// If space precedes a mark -> substitute a &nbsp; before the Mark, to prevent line breaking Test:
520					//-----------------------------------------------------------------------------------
521					for ($ptr = 1; $ptr < count($this->OTLdata); $ptr++) {
522						if ($this->OTLdata[$ptr]['general_category'] == Ucdn::UNICODE_GENERAL_CATEGORY_NON_SPACING_MARK && $this->OTLdata[$ptr - 1]['uni'] == 32) {
523							$this->OTLdata[$ptr - 1]['uni'] = 0xa0;
524							$this->OTLdata[$ptr - 1]['hex'] = '000A0';
525						}
526					}
527				} // 5(I). GSUB - Shaper - INDIC and SINHALA and KHMER
528				//===================================
529				elseif ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
530					$this->restrictToSyllable = true;
531					//-----------------------------------------------------------------------------------
532					// a. First decompose/compose split mattras
533					// (normalize) ??????? Nukta/Halant order etc ??????????????????????????????????????????????????????????????????????????
534					//-----------------------------------------------------------------------------------
535					for ($ptr = 0; $ptr < count($this->OTLdata); $ptr++) {
536						$char = $this->OTLdata[$ptr]['uni'];
537						$sub = Indic::decompose_indic($char);
538						if ($sub) {
539							$newinfo = [];
540							for ($i = 0; $i < count($sub); $i++) {
541								$newinfo[$i] = [];
542								$ucd_record = Ucdn::get_ucd_record($sub[$i]);
543								$newinfo[$i]['general_category'] = $ucd_record[0];
544								$newinfo[$i]['bidi_type'] = $ucd_record[2];
545								$charasstr = $this->unicode_hex($sub[$i]);
546								if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
547									$newinfo[$i]['group'] = 'M';
548								} else {
549									$newinfo[$i]['group'] = 'C';
550								}
551								$newinfo[$i]['uni'] = $sub[$i];
552								$newinfo[$i]['hex'] = $charasstr;
553							}
554							array_splice($this->OTLdata, $ptr, 1, $newinfo);
555							$ptr += count($sub) - 1;
556						}
557						/* Only Composition-exclusion exceptions that we want to recompose. */
558						if ($this->shaper == 'I') {
559							if ($char == 0x09AF && isset($this->OTLdata[$ptr + 1]) && $this->OTLdata[$ptr + 1]['uni'] == 0x09BC) {
560								$sub = 0x09DF;
561								$newinfo = [];
562								$newinfo[0] = [];
563								$ucd_record = Ucdn::get_ucd_record($sub);
564								$newinfo[0]['general_category'] = $ucd_record[0];
565								$newinfo[0]['bidi_type'] = $ucd_record[2];
566								$newinfo[0]['group'] = 'C';
567								$newinfo[0]['uni'] = $sub;
568								$newinfo[0]['hex'] = $this->unicode_hex($sub);
569								array_splice($this->OTLdata, $ptr, 2, $newinfo);
570							}
571						}
572					}
573					//-----------------------------------------------------------------------------------
574					// b. Analyse characters - group as syllables/clusters (Indic); invalid diacritics; add dotted circle
575					//-----------------------------------------------------------------------------------
576					$indic_category_string = '';
577					foreach ($this->OTLdata as $eid => $c) {
578						Indic::set_indic_properties($this->OTLdata[$eid], $scriptblock); // sets ['indic_category'] and ['indic_position']
579						//$c['general_category']
580						//$c['combining_class']
581						//$c['uni'] =  $char;
582
583						$indic_category_string .= Indic::$indic_category_char[$this->OTLdata[$eid]['indic_category']];
584					}
585
586					$broken_syllables = false;
587					if ($this->shaper == 'I') {
588						Indic::set_syllables($this->OTLdata, $indic_category_string, $broken_syllables);
589					} elseif ($this->shaper == 'S') {
590						Indic::set_syllables_sinhala($this->OTLdata, $indic_category_string, $broken_syllables);
591					} elseif ($this->shaper == 'K') {
592						Indic::set_syllables_khmer($this->OTLdata, $indic_category_string, $broken_syllables);
593					}
594					$indic_category_string = '';
595
596					//-----------------------------------------------------------------------------------
597					// c. Initial Re-ordering (Indic / Khmer / Sinhala)
598					//-----------------------------------------------------------------------------------
599					// Find base consonant
600					// Decompose/compose and reorder Matras
601					// Reorder marks to canonical order
602
603					$indic_config = Indic::$indic_configs[$scriptblock];
604					$dottedcircle = false;
605					if ($broken_syllables) {
606						if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'], 0x25CC)) {
607							$dottedcircle = [];
608							$ucd_record = Ucdn::get_ucd_record(0x25CC);
609							$dottedcircle[0]['general_category'] = $ucd_record[0];
610							$dottedcircle[0]['bidi_type'] = $ucd_record[2];
611							$dottedcircle[0]['group'] = 'C';
612							$dottedcircle[0]['uni'] = 0x25CC;
613							$dottedcircle[0]['indic_category'] = Indic::OT_DOTTEDCIRCLE;
614							$dottedcircle[0]['indic_position'] = Indic::POS_BASE_C;
615
616							$dottedcircle[0]['hex'] = '025CC';  // TEMPORARY *****
617						}
618					}
619					Indic::initial_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $indic_config, $scriptblock, $is_old_spec, $dottedcircle);
620
621					//-----------------------------------------------------------------------------------
622					// d. Apply initial and basic shaping forms GSUB Lookups (one at a time)
623					//-----------------------------------------------------------------------------------
624					if ($this->shaper == 'I' || $this->shaper == 'S') {
625						$tags = 'locl ccmp nukt akhn rphf rkrf pref blwf half pstf vatu cjct';
626					} elseif ($this->shaper == 'K') {
627						$tags = 'locl ccmp pref blwf abvf pstf cfar';
628					}
629					$this->_applyGSUBrulesIndic($tags, $GSUBscriptTag, $GSUBlangsys, $is_old_spec);
630
631					//-----------------------------------------------------------------------------------
632					// e. Final Re-ordering (Indic / Khmer / Sinhala)
633					//-----------------------------------------------------------------------------------
634					// Reorder matras
635					// Reorder reph
636					// Reorder pre-base reordering consonants:
637
638					Indic::final_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $indic_config, $scriptblock, $is_old_spec);
639
640					//-----------------------------------------------------------------------------------
641					// f. Apply 'init' feature to first syllable in word (indicated by ['mask']) Indic::FLAG(Indic::INIT);
642					//-----------------------------------------------------------------------------------
643					if ($this->shaper == 'I' || $this->shaper == 'S') {
644						$tags = 'init';
645						$this->_applyGSUBrulesIndic($tags, $GSUBscriptTag, $GSUBlangsys, $is_old_spec);
646					}
647
648					//-----------------------------------------------------------------------------------
649					// g. Apply Presentation Forms GSUB Lookups (+ any discretionary)
650					//-----------------------------------------------------------------------------------
651					$tags = 'pres abvs blws psts haln rlig calt liga clig mset';
652
653					$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';
654					$usetags = $tags;
655					if (!empty($this->mpdf->OTLtags)) {
656						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
657					}
658					if ($this->shaper == 'K') {  // Features are applied one at a time, working through each codepoint
659						$this->_applyGSUBrulesSingly($usetags, $GSUBscriptTag, $GSUBlangsys);
660					} else {
661						$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
662					}
663					$this->restrictToSyllable = false;
664				} // 5(M). GSUB - Shaper - MYANMAR (ONLY mym2)
665				//==============================
666				// NB Old style 'mymr' is left to go through the default shaper
667				elseif ($this->shaper == 'M') {
668					$this->restrictToSyllable = true;
669					//-----------------------------------------------------------------------------------
670					// a. Analyse characters - group as syllables/clusters (Myanmar); invalid diacritics; add dotted circle
671					//-----------------------------------------------------------------------------------
672					$myanmar_category_string = '';
673					foreach ($this->OTLdata as $eid => $c) {
674						Myanmar::set_myanmar_properties($this->OTLdata[$eid]); // sets ['myanmar_category'] and ['myanmar_position']
675						$myanmar_category_string .= Myanmar::$myanmar_category_char[$this->OTLdata[$eid]['myanmar_category']];
676					}
677					$broken_syllables = false;
678					Myanmar::set_syllables($this->OTLdata, $myanmar_category_string, $broken_syllables);
679					$myanmar_category_string = '';
680
681					//-----------------------------------------------------------------------------------
682					// b. Re-ordering (Myanmar mym2)
683					//-----------------------------------------------------------------------------------
684					$dottedcircle = false;
685					if ($broken_syllables) {
686						if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'], 0x25CC)) {
687							$dottedcircle = [];
688							$ucd_record = Ucdn::get_ucd_record(0x25CC);
689							$dottedcircle[0]['general_category'] = $ucd_record[0];
690							$dottedcircle[0]['bidi_type'] = $ucd_record[2];
691							$dottedcircle[0]['group'] = 'C';
692							$dottedcircle[0]['uni'] = 0x25CC;
693							$dottedcircle[0]['myanmar_category'] = Myanmar::OT_DOTTEDCIRCLE;
694							$dottedcircle[0]['myanmar_position'] = Myanmar::POS_BASE_C;
695							$dottedcircle[0]['hex'] = '025CC';
696						}
697					}
698					Myanmar::reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $dottedcircle);
699
700					//-----------------------------------------------------------------------------------
701					// c. Apply initial and basic shaping forms GSUB Lookups (one at a time)
702					//-----------------------------------------------------------------------------------
703
704					$tags = 'locl ccmp rphf pref blwf pstf';
705					$this->_applyGSUBrulesMyanmar($tags, $GSUBscriptTag, $GSUBlangsys);
706
707					//-----------------------------------------------------------------------------------
708					// d. Apply Presentation Forms GSUB Lookups (+ any discretionary)
709					//-----------------------------------------------------------------------------------
710					$tags = 'pres abvs blws psts haln rlig calt liga clig mset';
711					$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';
712					$usetags = $tags;
713					if (!empty($this->mpdf->OTLtags)) {
714						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
715					}
716					$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
717					$this->restrictToSyllable = false;
718				} // 5(E). GSUB - Shaper - SEA South East Asian (New Tai Lue, Cham, Tai Tam)
719				//==============================
720				elseif ($this->shaper == 'E') {
721					/* HarfBuzz says: If the designer designed the font for the 'DFLT' script,
722					 * use the default shaper.  Otherwise, use the SEA shaper.
723					 * Note that for some simple scripts, there may not be *any*
724					 * GSUB/GPOS needed, so there may be no scripts found! */
725
726					$this->restrictToSyllable = true;
727					//-----------------------------------------------------------------------------------
728					// a. Analyse characters - group as syllables/clusters (Indic); invalid diacritics; add dotted circle
729					//-----------------------------------------------------------------------------------
730					$sea_category_string = '';
731					foreach ($this->OTLdata as $eid => $c) {
732						Sea::set_sea_properties($this->OTLdata[$eid], $scriptblock); // sets ['sea_category'] and ['sea_position']
733						//$c['general_category']
734						//$c['combining_class']
735						//$c['uni'] =  $char;
736
737						$sea_category_string .= Sea::$sea_category_char[$this->OTLdata[$eid]['sea_category']];
738					}
739
740					$broken_syllables = false;
741					Sea::set_syllables($this->OTLdata, $sea_category_string, $broken_syllables);
742					$sea_category_string = '';
743
744					//-----------------------------------------------------------------------------------
745					// b. Apply locl and ccmp shaping forms - before initial re-ordering; GSUB Lookups (one at a time)
746					//-----------------------------------------------------------------------------------
747					$tags = 'locl ccmp';
748					$this->_applyGSUBrulesSingly($tags, $GSUBscriptTag, $GSUBlangsys);
749
750					//-----------------------------------------------------------------------------------
751					// c. Initial Re-ordering
752					//-----------------------------------------------------------------------------------
753					// Find base consonant
754					// Decompose/compose and reorder Matras
755					// Reorder marks to canonical order
756
757					$dottedcircle = false;
758					if ($broken_syllables) {
759						if ($this->mpdf->_charDefined($this->mpdf->fonts[$this->fontkey]['cw'], 0x25CC)) {
760							$dottedcircle = [];
761							$ucd_record = Ucdn::get_ucd_record(0x25CC);
762							$dottedcircle[0]['general_category'] = $ucd_record[0];
763							$dottedcircle[0]['bidi_type'] = $ucd_record[2];
764							$dottedcircle[0]['group'] = 'C';
765							$dottedcircle[0]['uni'] = 0x25CC;
766							$dottedcircle[0]['sea_category'] = Sea::OT_GB;
767							$dottedcircle[0]['sea_position'] = Sea::POS_BASE_C;
768
769							$dottedcircle[0]['hex'] = '025CC';  // TEMPORARY *****
770						}
771					}
772					Sea::initial_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $broken_syllables, $scriptblock, $dottedcircle);
773
774					//-----------------------------------------------------------------------------------
775					// d. Apply basic shaping forms GSUB Lookups (one at a time)
776					//-----------------------------------------------------------------------------------
777					$tags = 'pref abvf blwf pstf';
778					$this->_applyGSUBrulesSingly($tags, $GSUBscriptTag, $GSUBlangsys);
779
780					//-----------------------------------------------------------------------------------
781					// e. Final Re-ordering
782					//-----------------------------------------------------------------------------------
783
784					Sea::final_reordering($this->OTLdata, $this->GSUBdata[$this->GSUBfont], $scriptblock);
785
786					//-----------------------------------------------------------------------------------
787					// f. Apply Presentation Forms GSUB Lookups (+ any discretionary)
788					//-----------------------------------------------------------------------------------
789					$tags = 'pres abvs blws psts';
790
791					$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';
792					$usetags = $tags;
793					if (!empty($this->mpdf->OTLtags)) {
794						$usetags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
795					}
796					$this->_applyGSUBrules($usetags, $GSUBscriptTag, $GSUBlangsys);
797					$this->restrictToSyllable = false;
798				} // 5(D). GSUB - Shaper - DEFAULT (including THAI and LAO and MYANMAR v1 [mymr] and TIBETAN)
799				//==============================
800				else { // DEFAULT
801					//-----------------------------------------------------------------------------------
802					// a. First decompose/compose in Thai / Lao - Tibetan
803					//-----------------------------------------------------------------------------------
804					// Decomposition for THAI or LAO
805					/* This function implements the shaping logic documented here:
806					 *
807					 *   http://linux.thai.net/~thep/th-otf/shaping.html
808					 *
809					 * The first shaping rule listed there is needed even if the font has Thai
810					 * OpenType tables.
811					 *
812					 *
813					 * The following is NOT specified in the MS OT Thai spec, however, it seems
814					 * to be what Uniscribe and other engines implement.  According to Eric Muller:
815					 *
816					 * When you have a SARA AM, decompose it in NIKHAHIT + SARA AA, *and* move the
817					 * NIKHAHIT backwards over any tone mark (0E48-0E4B).
818					 *
819					 * <0E14, 0E4B, 0E33> -> <0E14, 0E4D, 0E4B, 0E32>
820					 *
821					 * This reordering is legit only when the NIKHAHIT comes from a SARA AM, not
822					 * when it's there to start with. The string <0E14, 0E4B, 0E4D> is probably
823					 * not what a user wanted, but the rendering is nevertheless nikhahit above
824					 * chattawa.
825					 *
826					 * Same for Lao.
827					 *
828					 *          Thai        Lao
829					 * SARA AM:     U+0E33  U+0EB3
830					 * SARA AA:     U+0E32  U+0EB2
831					 * Nikhahit:    U+0E4D  U+0ECD
832					 *
833					 * Testing shows that Uniscribe reorder the following marks:
834					 * Thai:    <0E31,0E34..0E37,0E47..0E4E>
835					 * Lao: <0EB1,0EB4..0EB7,0EC7..0ECE>
836					 *
837					 * Lao versions are the same as Thai + 0x80.
838					 */
839					if ($this->shaper == 'T' || $this->shaper == 'L') {
840						for ($ptr = 0; $ptr < count($this->OTLdata); $ptr++) {
841							$char = $this->OTLdata[$ptr]['uni'];
842							if (($char & ~0x0080) == 0x0E33) { // if SARA_AM (U+0E33 or U+0EB3)
843								$NIKHAHIT = $char + 0x1A;
844								$SARA_AA = $char - 1;
845								$sub = [$SARA_AA, $NIKHAHIT];
846
847								$newinfo = [];
848								$ucd_record = Ucdn::get_ucd_record($sub[0]);
849								$newinfo[0]['general_category'] = $ucd_record[0];
850								$newinfo[0]['bidi_type'] = $ucd_record[2];
851								$charasstr = $this->unicode_hex($sub[0]);
852								if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
853									$newinfo[0]['group'] = 'M';
854								} else {
855									$newinfo[0]['group'] = 'C';
856								}
857								$newinfo[0]['uni'] = $sub[0];
858								$newinfo[0]['hex'] = $charasstr;
859								$this->OTLdata[$ptr] = $newinfo[0]; // Substitute SARA_AM => SARA_AA
860
861								$ntones = 0; // number of (preceding) tone marks
862								// IS_TONE_MARK ((x) & ~0x0080, 0x0E34 - 0x0E37, 0x0E47 - 0x0E4E, 0x0E31)
863								while (isset($this->OTLdata[$ptr - 1 - $ntones]) && (
864								($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) == 0x0E31 ||
865								(($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) >= 0x0E34 &&
866								($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) <= 0x0E37) ||
867								(($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) >= 0x0E47 &&
868								($this->OTLdata[$ptr - 1 - $ntones]['uni'] & ~0x0080) <= 0x0E4E)
869								)
870								) {
871									$ntones++;
872								}
873
874								$newinfo = [];
875								$ucd_record = Ucdn::get_ucd_record($sub[1]);
876								$newinfo[0]['general_category'] = $ucd_record[0];
877								$newinfo[0]['bidi_type'] = $ucd_record[2];
878								$charasstr = $this->unicode_hex($sub[1]);
879								if (strpos($this->GlyphClassMarks, $charasstr) !== false) {
880									$newinfo[0]['group'] = 'M';
881								} else {
882									$newinfo[0]['group'] = 'C';
883								}
884								$newinfo[0]['uni'] = $sub[1];
885								$newinfo[0]['hex'] = $charasstr;
886								// Insert NIKAHIT
887								array_splice($this->OTLdata, $ptr - $ntones, 0, $newinfo);
888
889								$ptr++;
890							}
891						}
892					}
893
894					if ($scriptblock == Ucdn::SCRIPT_TIBETAN) {
895						// =========================
896						// Reordering TIBETAN
897						// =========================
898						// Tibetan does not need to need a shaper generally, as long as characters are presented in the correct order
899						// so we will do one minor change here:
900						// From ICU: If the present character is a number, and the next character is a pre-number combining mark
901						// then the two characters are reordered
902						// From MS OTL spec the following are Digit modifiers (Md): 0F18–0F19, 0F3E–0F3F
903						// Digits: 0F20–0F33
904						// On testing only 0x0F3F (pre-based mark) seems to need re-ordering
905						for ($ptr = 0; $ptr < count($this->OTLdata) - 1; $ptr++) {
906							if (Indic::in_range($this->OTLdata[$ptr]['uni'], 0x0F20, 0x0F33) && $this->OTLdata[$ptr + 1]['uni'] == 0x0F3F) {
907								$tmp = $this->OTLdata[$ptr + 1];
908								$this->OTLdata[$ptr + 1] = $this->OTLdata[$ptr];
909								$this->OTLdata[$ptr] = $tmp;
910							}
911						}
912
913
914						// =========================
915						// Decomposition for TIBETAN
916						// =========================
917						/* Recommended, but does not seem to change anything...
918						  for($ptr=0; $ptr<count($this->OTLdata); $ptr++) {
919						  $char = $this->OTLdata[$ptr]['uni'];
920						  $sub = Indic::decompose_indic($char);
921						  if ($sub) {
922						  $newinfo = array();
923						  for($i=0;$i<count($sub);$i++) {
924						  $newinfo[$i] = array();
925						  $ucd_record = Ucdn::get_ucd_record($sub[$i]);
926						  $newinfo[$i]['general_category'] = $ucd_record[0];
927						  $newinfo[$i]['bidi_type'] = $ucd_record[2];
928						  $charasstr = $this->unicode_hex($sub[$i]);
929						  if (strpos($this->GlyphClassMarks, $charasstr)!==false) { $newinfo[$i]['group'] =  'M'; }
930						  else { $newinfo[$i]['group'] =  'C'; }
931						  $newinfo[$i]['uni'] =  $sub[$i];
932						  $newinfo[$i]['hex'] =  $charasstr;
933						  }
934						  array_splice($this->OTLdata, $ptr, 1, $newinfo);
935						  $ptr += count($sub)-1;
936						  }
937						  }
938						 */
939					}
940
941
942					//-----------------------------------------------------------------------------------
943					// b. Apply all GSUB Lookups (in order specified in lookup list)
944					//-----------------------------------------------------------------------------------
945					$tags = 'locl ccmp pref blwf abvf pstf pres abvs blws psts haln rlig calt liga clig mset  RQD';
946					// pref blwf abvf pstf required for Tibetan
947					// " RQD" is a non-standard tag in Garuda font - presumably intended to be used by default ? "ReQuireD"
948					// Being a 3 letter tag is non-standard, and does not allow it to be set by font-feature-settings
949
950
951					/* ?Add these until shapers witten?
952					  Hangul:   ljmo vjmo tjmo
953					 */
954
955					$omittags = '';
956					$useGSUBtags = $tags;
957					if (!empty($this->mpdf->OTLtags)) {
958						$useGSUBtags = $this->_applyTagSettings($tags, $GSUBFeatures, $omittags, false);
959					}
960					// APPLY GSUB rules (as long as not Latin + SmallCaps - but not OTL smcp)
961					if (!(($this->mpdf->textvar & TextVars::FC_SMALLCAPS) && $scriptblock == Ucdn::SCRIPT_LATIN && strpos($useGSUBtags, 'smcp') === false)) {
962						$this->_applyGSUBrules($useGSUBtags, $GSUBscriptTag, $GSUBlangsys);
963					}
964				}
965			}
966
967			// Shapers - KHMER & THAI & LAO - Replace Word boundary marker with U+200B
968			// Also TIBETAN (no shaper)
969			//=======================================================
970			if (($this->shaper == "K" || $this->shaper == "T" || $this->shaper == "L") || $scriptblock == Ucdn::SCRIPT_TIBETAN) {
971				// Set up properties to insert a U+200B character
972				$newinfo = [];
973				//$newinfo[0] = array('general_category' => 1, 'bidi_type' => 14, 'group' => 'S', 'uni' => 0x200B, 'hex' => '0200B');
974				$newinfo[0] = [
975					'general_category' => Ucdn::UNICODE_GENERAL_CATEGORY_FORMAT,
976					'bidi_type' => Ucdn::BIDI_CLASS_BN,
977					'group' => 'S', 'uni' => 0x200B, 'hex' => '0200B'];
978				// Then insert U+200B at (after) all word end boundaries
979				for ($i = count($this->OTLdata) - 1; $i > 0; $i--) {
980					// Make sure after GSUB that wordend has not been moved - check next char is not in the same syllable
981					if (isset($this->OTLdata[$i]['wordend']) && $this->OTLdata[$i]['wordend'] &&
982						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'])) {
983						array_splice($this->OTLdata, $i + 1, 0, $newinfo);
984						$this->_updateLigatureMarks($i, 1);
985					} elseif ($this->OTLdata[$i]['uni'] == 0x2e) { // Word end if Full-stop.
986						array_splice($this->OTLdata, $i + 1, 0, $newinfo);
987						$this->_updateLigatureMarks($i, 1);
988					}
989				}
990			}
991
992
993			// Shapers - INDIC & ARABIC & KHMER & SINHALA  & MYANMAR - Remove ZWJ and ZWNJ
994			//=======================================================
995			if ($this->shaper == 'I' || $this->shaper == 'S' || $this->shaper == 'A' || $this->shaper == 'K' || $this->shaper == 'M') {
996				// Remove ZWJ and ZWNJ
997				for ($i = 0; $i < count($this->OTLdata); $i++) {
998					if ($this->OTLdata[$i]['uni'] == 8204 || $this->OTLdata[$i]['uni'] == 8205) {
999						array_splice($this->OTLdata, $i, 1);
1000						$this->_updateLigatureMarks($i, -1);
1001					}
1002				}
1003			}
1004
1005
1006			////////////////////////////////////////////////////////////////
1007			////////////////////////////////////////////////////////////////
1008			//////////       GPOS          /////////////////////////////////
1009			////////////////////////////////////////////////////////////////
1010			////////////////////////////////////////////////////////////////
1011			if (($useOTL & 0xFF) && $GPOSscriptTag && $GPOSlangsys && $GPOSFeatures) {
1012				$this->Entry = [];
1013				$this->Exit = [];
1014
1015				// 6. Load GPOS data, Coverage & Lookups
1016				//=================================================================
1017				if (!isset($this->GPOSdata[$this->fontkey])) {
1018					include $this->fontCache->tempFilename($this->mpdf->CurrentFont['fontkey'] . '.GPOSdata.php');
1019					$this->LuCoverage = $this->GPOSdata[$this->fontkey]['LuCoverage'] = $LuCoverage;
1020				} else {
1021					$this->LuCoverage = $this->GPOSdata[$this->fontkey]['LuCoverage'];
1022				}
1023
1024				$this->GPOSLookups = $this->mpdf->CurrentFont['GPOSLookups'];
1025
1026
1027				// 7. Select Feature tags to use (incl optional)
1028				//==============================
1029				$tags = 'abvm blwm mark mkmk curs cpsp dist requ'; // Default set
1030				// 'requ' is not listed in the Microsoft registry of Feature tags
1031				// Found in Arial Unicode MS, it repositions the baseline for punctuation in Kannada script
1032
1033				// ZZZ96
1034				// Set kern to be included by default in non-Latin script (? just when shapers used)
1035				// Kern is used in some fonts to reposition marks etc. and is essential for correct display
1036				//if ($this->shaper) {$tags .= ' kern'; }
1037				if ($scriptblock != Ucdn::SCRIPT_LATIN) {
1038					$tags .= ' kern';
1039				}
1040
1041				$omittags = '';
1042				$usetags = $tags;
1043				if (!empty($this->mpdf->OTLtags)) {
1044					$usetags = $this->_applyTagSettings($tags, $GPOSFeatures, $omittags, false);
1045				}
1046
1047
1048
1049				// 8. Get GPOS LookupList from Feature tags
1050				//==============================
1051				$LookupList = [];
1052				foreach ($GPOSFeatures as $tag => $arr) {
1053					if (strpos($usetags, $tag) !== false) {
1054						foreach ($arr as $lu) {
1055							$LookupList[$lu] = $tag;
1056						}
1057					}
1058				}
1059				ksort($LookupList);
1060
1061
1062				// 9. Apply GPOS Lookups (in order specified in lookup list but selecting from specified tags)
1063				//==============================
1064				// APPLY THE GPOS RULES (as long as not Latin + SmallCaps - but not OTL smcp)
1065				if (!(($this->mpdf->textvar & TextVars::FC_SMALLCAPS) && $scriptblock == Ucdn::SCRIPT_LATIN && strpos($useGSUBtags, 'smcp') === false)) {
1066					$this->_applyGPOSrules($LookupList, $is_old_spec);
1067					// (sets: $this->OTLdata[n]['GPOSinfo'] XPlacement YPlacement XAdvance Entry Exit )
1068				}
1069
1070				// 10. Process cursive text
1071				//==============================
1072				if (count($this->Entry) || count($this->Exit)) {
1073					// RTL
1074					$incurs = false;
1075					for ($i = (count($this->OTLdata) - 1); $i >= 0; $i--) {
1076						if (isset($this->Entry[$i]) && isset($this->Entry[$i]['Y']) && $this->Entry[$i]['dir'] == 'RTL') {
1077							$nextbase = $i - 1; // Set as next base ignoring marks (next base reading RTL in logical oder
1078							while (isset($this->OTLdata[$nextbase]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex']) !== false) {
1079								$nextbase--;
1080							}
1081							if (isset($this->Exit[$nextbase]) && isset($this->Exit[$nextbase]['Y'])) {
1082								$diff = $this->Entry[$i]['Y'] - $this->Exit[$nextbase]['Y'];
1083								if ($incurs === false) {
1084									$incurs = $diff;
1085								} else {
1086									$incurs += $diff;
1087								}
1088								for ($j = ($i - 1); $j >= $nextbase; $j--) {
1089									if (isset($this->OTLdata[$j]['GPOSinfo']['YPlacement'])) {
1090										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] += $incurs;
1091									} else {
1092										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] = $incurs;
1093									}
1094								}
1095								if (isset($this->Exit[$i]['X']) && isset($this->Entry[$nextbase]['X'])) {
1096									$adj = -($this->Entry[$i]['X'] - $this->Exit[$nextbase]['X']);
1097									// If XAdvance is aplied - in order for PDF to position the Advance correctly need to place it on:
1098									// in RTL - the current glyph or the last of any associated marks
1099									if (isset($this->OTLdata[$nextbase + 1]['GPOSinfo']['XAdvance'])) {
1100										$this->OTLdata[$nextbase + 1]['GPOSinfo']['XAdvance'] += $adj;
1101									} else {
1102										$this->OTLdata[$nextbase + 1]['GPOSinfo']['XAdvance'] = $adj;
1103									}
1104								}
1105							} else {
1106								$incurs = false;
1107							}
1108						} elseif (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex']) !== false) {
1109							continue;
1110						} // ignore Marks
1111						else {
1112							$incurs = false;
1113						}
1114					}
1115					// LTR
1116					$incurs = false;
1117					for ($i = 0; $i < count($this->OTLdata); $i++) {
1118						if (isset($this->Exit[$i]) && isset($this->Exit[$i]['Y']) && $this->Exit[$i]['dir'] == 'LTR') {
1119							$nextbase = $i + 1; // Set as next base ignoring marks
1120							while (strpos($this->GlyphClassMarks, $this->OTLdata[$nextbase]['hex']) !== false) {
1121								$nextbase++;
1122							}
1123							if (isset($this->Entry[$nextbase]) && isset($this->Entry[$nextbase]['Y'])) {
1124								$diff = $this->Exit[$i]['Y'] - $this->Entry[$nextbase]['Y'];
1125								if ($incurs === false) {
1126									$incurs = $diff;
1127								} else {
1128									$incurs += $diff;
1129								}
1130								for ($j = ($i + 1); $j <= $nextbase; $j++) {
1131									if (isset($this->OTLdata[$j]['GPOSinfo']['YPlacement'])) {
1132										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] += $incurs;
1133									} else {
1134										$this->OTLdata[$j]['GPOSinfo']['YPlacement'] = $incurs;
1135									}
1136								}
1137								if (isset($this->Exit[$i]['X']) && isset($this->Entry[$nextbase]['X'])) {
1138									$adj = -($this->Exit[$i]['X'] - $this->Entry[$nextbase]['X']);
1139									// If XAdvance is aplied - in order for PDF to position the Advance correctly need to place it on:
1140									// in LTR - the next glyph, ignoring marks
1141									if (isset($this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'])) {
1142										$this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'] += $adj;
1143									} else {
1144										$this->OTLdata[$nextbase]['GPOSinfo']['XAdvance'] = $adj;
1145									}
1146								}
1147							} else {
1148								$incurs = false;
1149							}
1150						} elseif (strpos($this->GlyphClassMarks, $this->OTLdata[$i]['hex']) !== false) {
1151							continue;
1152						} // ignore Marks
1153						else {
1154							$incurs = false;
1155						}
1156					}
1157				}
1158			} // end GPOS
1159
1160			if ($this->debugOTL) {
1161				$this->_dumpproc('END', '-', '-', '-', '-', 0, '-', 0);
1162				exit;
1163			}
1164
1165			$this->schOTLdata[$sch] = $this->OTLdata;
1166			$this->OTLdata = [];
1167		} // END foreach subchunk
1168		// 11. Re-assemble and return text string
1169		//==============================
1170		$newGPOSinfo = [];
1171		$newOTLdata = [];
1172		$newchar_data = [];
1173		$newgroup = '';
1174		$e = '';
1175		$ectr = 0;
1176
1177		for ($sch = 0; $sch <= $subchunk; $sch++) {
1178			for ($i = 0; $i < count($this->schOTLdata[$sch]); $i++) {
1179				if (isset($this->schOTLdata[$sch][$i]['GPOSinfo'])) {
1180					$newGPOSinfo[$ectr] = $this->schOTLdata[$sch][$i]['GPOSinfo'];
1181				}
1182				$newchar_data[$ectr] = ['bidi_class' => $this->schOTLdata[$sch][$i]['bidi_type'], 'uni' => $this->schOTLdata[$sch][$i]['uni']];
1183				$newgroup .= $this->schOTLdata[$sch][$i]['group'];
1184				$e.=code2utf($this->schOTLdata[$sch][$i]['uni']);
1185				if (isset($this->mpdf->CurrentFont['subset'])) {
1186					$this->mpdf->CurrentFont['subset'][$this->schOTLdata[$sch][$i]['uni']] = $this->schOTLdata[$sch][$i]['uni'];
1187				}
1188				$ectr++;
1189			}
1190		}
1191		$this->OTLdata['GPOSinfo'] = $newGPOSinfo;
1192		$this->OTLdata['char_data'] = $newchar_data;
1193		$this->OTLdata['group'] = $newgroup;
1194
1195
1196		// This leaves OTLdata::GPOSinfo, ::bidi_type, & ::group
1197
1198		return $e;
1199	}
1200
1201	function _applyTagSettings($tags, $Features, $omittags = '', $onlytags = false)
1202	{
1203		if (empty($this->mpdf->OTLtags['Plus']) && empty($this->mpdf->OTLtags['Minus']) && empty($this->mpdf->OTLtags['FFPlus']) && empty($this->mpdf->OTLtags['FFMinus'])) {
1204			return $tags;
1205		}
1206
1207		// Use $tags as starting point
1208		$usetags = $tags;
1209
1210		// Only set / unset tags which are in the font
1211		// Ignore tags which are in $omittags
1212		// If $onlytags, then just unset tags which are already in the Tag list
1213
1214		$fp = $fm = $ffp = $ffm = '';
1215
1216		// Font features to enable - set by font-variant-xx
1217		if (isset($this->mpdf->OTLtags['Plus'])) {
1218			$fp = $this->mpdf->OTLtags['Plus'];
1219		}
1220		preg_match_all('/([a-zA-Z0-9]{4})/', $fp, $m);
1221		for ($i = 0; $i < count($m[0]); $i++) {
1222			$t = $m[1][$i];
1223			// Is it a valid tag?
1224			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1225				$usetags .= ' ' . $t;
1226			}
1227		}
1228
1229		// Font features to disable - set by font-variant-xx
1230		if (isset($this->mpdf->OTLtags['Minus'])) {
1231			$fm = $this->mpdf->OTLtags['Minus'];
1232		}
1233		preg_match_all('/([a-zA-Z0-9]{4})/', $fm, $m);
1234		for ($i = 0; $i < count($m[0]); $i++) {
1235			$t = $m[1][$i];
1236			// Is it a valid tag?
1237			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1238				$usetags = str_replace($t, '', $usetags);
1239			}
1240		}
1241
1242		// Font features to enable - set by font-feature-settings
1243		if (isset($this->mpdf->OTLtags['FFPlus'])) {
1244			$ffp = $this->mpdf->OTLtags['FFPlus']; // Font Features - may include integer: salt4
1245		}
1246		preg_match_all('/([a-zA-Z0-9]{4})([\d+]*)/', $ffp, $m);
1247		for ($i = 0; $i < count($m[0]); $i++) {
1248			$t = $m[1][$i];
1249			// Is it a valid tag?
1250			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1251				$usetags .= ' ' . $m[0][$i];  //  - may include integer: salt4
1252			}
1253		}
1254
1255		// Font features to disable - set by font-feature-settings
1256		if (isset($this->mpdf->OTLtags['FFMinus'])) {
1257			$ffm = $this->mpdf->OTLtags['FFMinus'];
1258		}
1259		preg_match_all('/([a-zA-Z0-9]{4})/', $ffm, $m);
1260		for ($i = 0; $i < count($m[0]); $i++) {
1261			$t = $m[1][$i];
1262			// Is it a valid tag?
1263			if (isset($Features[$t]) && strpos($omittags, $t) === false && (!$onlytags || strpos($tags, $t) !== false )) {
1264				$usetags = str_replace($t, '', $usetags);
1265			}
1266		}
1267		return $usetags;
1268	}
1269
1270	function _applyGSUBrules($usetags, $scriptTag, $langsys)
1271	{
1272		// Features from all Tags are applied together, in Lookup List order.
1273		// For Indic - should be applied one syllable at a time
1274		// - Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1275		// if $this->restrictToSyllable is true
1276
1277		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1278		$LookupList = [];
1279		foreach ($GSUBFeatures as $tag => $arr) {
1280			if (strpos($usetags, $tag) !== false) {
1281				foreach ($arr as $lu) {
1282					$LookupList[$lu] = $tag;
1283				}
1284			}
1285		}
1286		ksort($LookupList);
1287
1288		foreach ($LookupList as $lu => $tag) {
1289			$Type = $this->GSUBLookups[$lu]['Type'];
1290			$Flag = $this->GSUBLookups[$lu]['Flag'];
1291			$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1292			$tagInt = 1;
1293			if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1294				$tagInt = $m[1];
1295			}
1296			$ptr = 0;
1297			// Test each glyph sequentially
1298			while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1299				$currGlyph = $this->OTLdata[$ptr]['hex'];
1300				$currGID = $this->OTLdata[$ptr]['uni'];
1301				$shift = 1;
1302				foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1303					// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1304					if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1305						// Get rules from font GSUB subtable
1306						$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $tag, 0, $tagInt);
1307
1308						if ($shift) {
1309							break;
1310						}
1311					}
1312				}
1313				if ($shift == 0) {
1314					$shift = 1;
1315				}
1316				$ptr += $shift;
1317			}
1318		}
1319	}
1320
1321	function _applyGSUBrulesSingly($usetags, $scriptTag, $langsys)
1322	{
1323		// Features are applied one at a time, working through each codepoint
1324
1325		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1326
1327		$tags = explode(' ', $usetags);
1328		foreach ($tags as $usetag) {
1329			$LookupList = [];
1330			foreach ($GSUBFeatures as $tag => $arr) {
1331				if (strpos($usetags, $tag) !== false) {
1332					foreach ($arr as $lu) {
1333						$LookupList[$lu] = $tag;
1334					}
1335				}
1336			}
1337			ksort($LookupList);
1338
1339			$ptr = 0;
1340			// Test each glyph sequentially
1341			while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1342				$currGlyph = $this->OTLdata[$ptr]['hex'];
1343				$currGID = $this->OTLdata[$ptr]['uni'];
1344				$shift = 1;
1345
1346				foreach ($LookupList as $lu => $tag) {
1347					$Type = $this->GSUBLookups[$lu]['Type'];
1348					$Flag = $this->GSUBLookups[$lu]['Flag'];
1349					$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1350					$tagInt = 1;
1351					if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1352						$tagInt = $m[1];
1353					}
1354
1355					foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1356						// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1357						if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1358							// Get rules from font GSUB subtable
1359							$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $tag, 0, $tagInt);
1360
1361							if ($shift) {
1362								break 2;
1363							}
1364						}
1365					}
1366				}
1367				if ($shift == 0) {
1368					$shift = 1;
1369				}
1370				$ptr += $shift;
1371			}
1372		}
1373	}
1374
1375	function _applyGSUBrulesMyanmar($usetags, $scriptTag, $langsys)
1376	{
1377		// $usetags = locl ccmp rphf pref blwf pstf';
1378		// applied to all characters
1379
1380		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1381
1382		// ALL should be applied one syllable at a time
1383		// Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1384		$tags = explode(' ', $usetags);
1385		foreach ($tags as $usetag) {
1386			$LookupList = [];
1387			foreach ($GSUBFeatures as $tag => $arr) {
1388				if ($tag == $usetag) {
1389					foreach ($arr as $lu) {
1390						$LookupList[$lu] = $tag;
1391					}
1392				}
1393			}
1394			ksort($LookupList);
1395
1396			foreach ($LookupList as $lu => $tag) {
1397				$Type = $this->GSUBLookups[$lu]['Type'];
1398				$Flag = $this->GSUBLookups[$lu]['Flag'];
1399				$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1400				$tagInt = 1;
1401				if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1402					$tagInt = $m[1];
1403				}
1404
1405				$ptr = 0;
1406				// Test each glyph sequentially
1407				while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1408					$currGlyph = $this->OTLdata[$ptr]['hex'];
1409					$currGID = $this->OTLdata[$ptr]['uni'];
1410					$shift = 1;
1411					foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1412						// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1413						if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1414							// Get rules from font GSUB subtable
1415							$shift = $this->_applyGSUBsubtable($lu, $c, $ptr, $currGlyph, $currGID, ($subtable_offset - $this->GSUB_offset), $Type, $Flag, $MarkFilteringSet, $this->GSLuCoverage[$lu][$c], 0, $usetag, 0, $tagInt);
1416
1417							if ($shift) {
1418								break;
1419							}
1420						}
1421					}
1422					if ($shift == 0) {
1423						$shift = 1;
1424					}
1425					$ptr += $shift;
1426				}
1427			}
1428		}
1429	}
1430
1431	function _applyGSUBrulesIndic($usetags, $scriptTag, $langsys, $is_old_spec)
1432	{
1433		// $usetags = 'locl ccmp nukt akhn rphf rkrf pref blwf half pstf vatu cjct'; then later - init
1434		// rphf, pref, blwf, half, abvf, pstf, and init are only applied where ['mask'] indicates:  Indic::FLAG(Indic::RPHF);
1435		// The rest are applied to all characters
1436
1437		$GSUBFeatures = $this->mpdf->CurrentFont['GSUBFeatures'][$scriptTag][$langsys];
1438
1439		// ALL should be applied one syllable at a time
1440		// Implemented in functions checkContextMatch and checkContextMatchMultiple by failing to match if outside scope of current 'syllable'
1441		$tags = explode(' ', $usetags);
1442		foreach ($tags as $usetag) {
1443			$LookupList = [];
1444			foreach ($GSUBFeatures as $tag => $arr) {
1445				if ($tag == $usetag) {
1446					foreach ($arr as $lu) {
1447						$LookupList[$lu] = $tag;
1448					}
1449				}
1450			}
1451			ksort($LookupList);
1452
1453			foreach ($LookupList as $lu => $tag) {
1454				$Type = $this->GSUBLookups[$lu]['Type'];
1455				$Flag = $this->GSUBLookups[$lu]['Flag'];
1456				$MarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1457				$tagInt = 1;
1458				if (preg_match('/' . $tag . '([0-9]{1,2})/', $usetags, $m)) {
1459					$tagInt = $m[1];
1460				}
1461
1462				$ptr = 0;
1463				// Test each glyph sequentially
1464				while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
1465					$currGlyph = $this->OTLdata[$ptr]['hex'];
1466					$currGID = $this->OTLdata[$ptr]['uni'];
1467					$shift = 1;
1468					foreach ($this->GSUBLookups[$lu]['Subtables'] as $c => $subtable_offset) {
1469						// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
1470						if (isset($this->GSLuCoverage[$lu][$c][$currGID])) {
1471							if (strpos('rphf pref blwf half pstf cfar init', $usetag) !== false) { // only apply when mask indicates
1472								$mask = 0;
1473								switch ($usetag) {
1474									case 'rphf':
1475										$mask = (1 << (Indic::RPHF));
1476										break;
1477									case 'pref':
1478										$mask = (1 << (Indic::PREF));
1479										break;
1480									case 'blwf':
1481										$mask = (1 << (Indic::BLWF));
1482										break;
1483									case 'half':
1484										$mask = (1 << (Indic::HALF));
1485										break;
1486									case 'pstf':
1487										$mask = (1 << (Indic::PSTF));
1488										break;
1489									case 'cfar':
1490										$mask = (1 << (Indic::CFAR));
1491										break;
1492									case 'init':
1493										$mask = (1 << (Indic::INIT));
1494										break;
1495								}
1496								if (!($this->OTLdata[$ptr]['mask'] & $mask)) {
1497									continue;
1498								}
1499							}
1500							// Get rules from font GSUB subtable
1501							$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);
1502
1503							if ($shift) {
1504								break;
1505							}
1506						} // Special case for Indic  ZZZ99S
1507						// Check to substitute Halant-Consonant in PREF, BLWF or PSTF
1508						// i.e. new spec but GSUB tables have Consonant-Halant in Lookups e.g. FreeSerif, which
1509						// incorrectly just moved old spec tables to new spec. Uniscribe seems to cope with this
1510						// See also ttffontsuni.php
1511						// First check if current glyph is a Halant/Virama
1512						elseif (static::_OTL_OLD_SPEC_COMPAT_1 && $Type == 4 && !$is_old_spec && strpos('0094D 009CD 00A4D 00ACD 00B4D 00BCD 00C4D 00CCD 00D4D', $currGlyph) !== false) {
1513							// only apply when 'pref blwf pstf' tags, and when mask indicates
1514							if (strpos('pref blwf pstf', $usetag) !== false) {
1515								$mask = 0;
1516								switch ($usetag) {
1517									case 'pref':
1518										$mask = (1 << (Indic::PREF));
1519										break;
1520									case 'blwf':
1521										$mask = (1 << (Indic::BLWF));
1522										break;
1523									case 'pstf':
1524										$mask = (1 << (Indic::PSTF));
1525										break;
1526								}
1527								if (!($this->OTLdata[$ptr]['mask'] & $mask)) {
1528									continue;
1529								}
1530
1531								$nextGlyph = $this->OTLdata[$ptr + 1]['hex'];
1532								$nextGID = $this->OTLdata[$ptr + 1]['uni'];
1533								if (isset($this->GSLuCoverage[$lu][$c][$nextGID])) {
1534									// Get rules from font GSUB subtable
1535									$shift = $this->_applyGSUBsubtableSpecial($lu, $c, $ptr, $currGlyph, $currGID, $nextGlyph, $nextGID, ($subtable_offset - $this->GSUB_offset), $Type, $this->GSLuCoverage[$lu][$c]);
1536
1537									if ($shift) {
1538										break;
1539									}
1540								}
1541							}
1542						}
1543					}
1544					if ($shift == 0) {
1545						$shift = 1;
1546					}
1547					$ptr += $shift;
1548				}
1549			}
1550		}
1551	}
1552
1553	function _applyGSUBsubtableSpecial($lookupID, $subtable, $ptr, $currGlyph, $currGID, $nextGlyph, $nextGID, $subtable_offset, $Type, $LuCoverage)
1554	{
1555
1556		// Special case for Indic
1557		// Check to substitute Halant-Consonant in PREF, BLWF or PSTF
1558		// i.e. new spec but GSUB tables have Consonant-Halant in Lookups e.g. FreeSerif, which
1559		// incorrectly just moved old spec tables to new spec. Uniscribe seems to cope with this
1560		// See also ttffontsuni.php
1561
1562		$this->seek($subtable_offset);
1563		$SubstFormat = $this->read_ushort();
1564
1565		// Subtable contains Consonant - Halant
1566		// Text string contains Halant ($CurrGlyph) - Consonant ($nextGlyph)
1567		// Halant has already been matched, and already checked that $nextGID is in Coverage table
1568		////////////////////////////////////////////////////////////////////////////////
1569		// Only does: LookupType 4: Ligature Substitution Subtable : n to 1
1570		////////////////////////////////////////////////////////////////////////////////
1571		$Coverage = $subtable_offset + $this->read_ushort();
1572		$NextGlyphPos = $LuCoverage[$nextGID];
1573		$LigSetCount = $this->read_short();
1574
1575		$this->skip($NextGlyphPos * 2);
1576		$LigSet = $subtable_offset + $this->read_short();
1577
1578		$this->seek($LigSet);
1579		$LigCount = $this->read_short();
1580		// LigatureSet i.e. all starting with the same Glyph $nextGlyph [Consonant]
1581		$LigatureOffset = [];
1582		for ($g = 0; $g < $LigCount; $g++) {
1583			$LigatureOffset[$g] = $LigSet + $this->read_ushort();
1584		}
1585		for ($g = 0; $g < $LigCount; $g++) {
1586			// Ligature tables
1587			$this->seek($LigatureOffset[$g]);
1588			$LigGlyph = $this->read_ushort();
1589			$substitute = $this->glyphToChar($LigGlyph);
1590			$CompCount = $this->read_ushort();
1591
1592			if ($CompCount != 2) {
1593				return 0;
1594			} // Only expecting to work with 2:1 (and no ignore characters in between)
1595
1596
1597			$gid = $this->read_ushort();
1598			$checkGlyph = $this->glyphToChar($gid); // Other component/input Glyphs starting at position 2 (arrayindex 1)
1599
1600			if ($currGID == $checkGlyph) {
1601				$match = true;
1602			} else {
1603				$match = false;
1604				break;
1605			}
1606
1607			$GlyphPos = [];
1608			$GlyphPos[] = $ptr;
1609			$GlyphPos[] = $ptr + 1;
1610
1611
1612			if ($match) {
1613				$shift = $this->GSUBsubstitute($ptr, $substitute, 4, $GlyphPos); // GlyphPos contains positions to set null
1614				if ($shift) {
1615					return 1;
1616				}
1617			}
1618		}
1619		return 0;
1620	}
1621
1622	function _applyGSUBsubtable($lookupID, $subtable, $ptr, $currGlyph, $currGID, $subtable_offset, $Type, $Flag, $MarkFilteringSet, $LuCoverage, $level, $currentTag, $is_old_spec, $tagInt)
1623	{
1624		$ignore = $this->_getGCOMignoreString($Flag, $MarkFilteringSet);
1625
1626		// Lets start
1627		$this->seek($subtable_offset);
1628		$SubstFormat = $this->read_ushort();
1629
1630		////////////////////////////////////////////////////////////////////////////////
1631		// LookupType 1: Single Substitution Subtable : 1 to 1
1632		////////////////////////////////////////////////////////////////////////////////
1633		if ($Type == 1) {
1634			// Flag = Ignore
1635			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1636				return 0;
1637			}
1638			$CoverageOffset = $subtable_offset + $this->read_ushort();
1639			$GlyphPos = $LuCoverage[$currGID];
1640			//===========
1641			// Format 1:
1642			//===========
1643			if ($SubstFormat == 1) { // Calculated output glyph indices
1644				$DeltaGlyphID = $this->read_short();
1645				$this->seek($CoverageOffset);
1646				$glyphs = $this->_getCoverageGID();
1647				$GlyphID = $glyphs[$GlyphPos] + $DeltaGlyphID;
1648			} //===========
1649			// Format 2:
1650			//===========
1651			elseif ($SubstFormat == 2) { // Specified output glyph indices
1652				$GlyphCount = $this->read_ushort();
1653				$this->skip($GlyphPos * 2);
1654				$GlyphID = $this->read_ushort();
1655			}
1656
1657			$substitute = $this->glyphToChar($GlyphID);
1658			$shift = $this->GSUBsubstitute($ptr, $substitute, $Type);
1659			if ($this->debugOTL && $shift) {
1660				$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1661			}
1662			if ($shift) {
1663				return 1;
1664			}
1665			return 0;
1666		} ////////////////////////////////////////////////////////////////////////////////
1667		// LookupType 2: Multiple Substitution Subtable : 1 to n
1668		////////////////////////////////////////////////////////////////////////////////
1669		elseif ($Type == 2) {
1670			// Flag = Ignore
1671			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1672				return 0;
1673			}
1674			$Coverage = $subtable_offset + $this->read_ushort();
1675			$GlyphPos = $LuCoverage[$currGID];
1676			$this->skip(2);
1677			$this->skip($GlyphPos * 2);
1678			$Sequences = $subtable_offset + $this->read_short();
1679
1680			$this->seek($Sequences);
1681			$GlyphCount = $this->read_short();
1682			$SubstituteGlyphs = [];
1683			for ($g = 0; $g < $GlyphCount; $g++) {
1684				$sgid = $this->read_ushort();
1685				$SubstituteGlyphs[] = $this->glyphToChar($sgid);
1686			}
1687
1688			$shift = $this->GSUBsubstitute($ptr, $SubstituteGlyphs, $Type);
1689			if ($this->debugOTL && $shift) {
1690				$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1691			}
1692			if ($shift) {
1693				return $shift;
1694			}
1695			return 0;
1696		} ////////////////////////////////////////////////////////////////////////////////
1697		// LookupType 3: Alternate Forms : 1 to 1(n)
1698		////////////////////////////////////////////////////////////////////////////////
1699		elseif ($Type == 3) {
1700			// Flag = Ignore
1701			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1702				return 0;
1703			}
1704			$Coverage = $subtable_offset + $this->read_ushort();
1705			$AlternateSetCount = $this->read_short();
1706			///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1707			// Need to set alternate IF set by CSS3 font-feature for a tag
1708			// i.e. if this is 'salt' alternate may be set to 2
1709			// default value will be $alt=1 ( === index of 0 in list of alternates)
1710			$alt = 1; // $alt=1 points to Alternative[0]
1711			if ($tagInt > 1) {
1712				$alt = $tagInt;
1713			}
1714			///////////////////////////////////////////////////////////////////////////////!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
1715			if ($alt == 0) {
1716				return 0;
1717			} // If specified alternate not present, cancel [ or could default $alt = 1 ?]
1718
1719			$GlyphPos = $LuCoverage[$currGID];
1720			$this->skip($GlyphPos * 2);
1721
1722			$AlternateSets = $subtable_offset + $this->read_short();
1723			$this->seek($AlternateSets);
1724
1725			$AlternateGlyphCount = $this->read_short();
1726			if ($alt > $AlternateGlyphCount) {
1727				return 0;
1728			} // If specified alternate not present, cancel [ or could default $alt = 1 ?]
1729
1730			$this->skip(($alt - 1) * 2);
1731			$GlyphID = $this->read_ushort();
1732
1733			$substitute = $this->glyphToChar($GlyphID);
1734			$shift = $this->GSUBsubstitute($ptr, $substitute, $Type);
1735			if ($this->debugOTL && $shift) {
1736				$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1737			}
1738			if ($shift) {
1739				return 1;
1740			}
1741			return 0;
1742		} ////////////////////////////////////////////////////////////////////////////////
1743		// LookupType 4: Ligature Substitution Subtable : n to 1
1744		////////////////////////////////////////////////////////////////////////////////
1745		elseif ($Type == 4) {
1746			// Flag = Ignore
1747			if ($this->_checkGCOMignore($Flag, $currGlyph, $MarkFilteringSet)) {
1748				return 0;
1749			}
1750			$Coverage = $subtable_offset + $this->read_ushort();
1751			$FirstGlyphPos = $LuCoverage[$currGID];
1752
1753			$LigSetCount = $this->read_short();
1754
1755			$this->skip($FirstGlyphPos * 2);
1756			$LigSet = $subtable_offset + $this->read_short();
1757
1758			$this->seek($LigSet);
1759			$LigCount = $this->read_short();
1760			// LigatureSet i.e. all starting with the same first Glyph $currGlyph
1761			$LigatureOffset = [];
1762			for ($g = 0; $g < $LigCount; $g++) {
1763				$LigatureOffset[$g] = $LigSet + $this->read_ushort();
1764			}
1765			for ($g = 0; $g < $LigCount; $g++) {
1766				// Ligature tables
1767				$this->seek($LigatureOffset[$g]);
1768				$LigGlyph = $this->read_ushort(); // Output Ligature GlyphID
1769				$substitute = $this->glyphToChar($LigGlyph);
1770				$CompCount = $this->read_ushort();
1771
1772				$spos = $ptr;
1773				$match = true;
1774				$GlyphPos = [];
1775				$GlyphPos[] = $spos;
1776				for ($l = 1; $l < $CompCount; $l++) {
1777					$gid = $this->read_ushort();
1778					$checkGlyph = $this->glyphToChar($gid); // Other component/input Glyphs starting at position 2 (arrayindex 1)
1779
1780					$spos++;
1781					//while $this->OTLdata[$spos]['uni'] is an "ignore" =>  spos++
1782					while (isset($this->OTLdata[$spos]) && strpos($ignore, $this->OTLdata[$spos]['hex']) !== false) {
1783						$spos++;
1784					}
1785
1786					if (isset($this->OTLdata[$spos]) && $this->OTLdata[$spos]['uni'] == $checkGlyph) {
1787						$GlyphPos[] = $spos;
1788					} else {
1789						$match = false;
1790						break;
1791					}
1792				}
1793
1794
1795				if ($match) {
1796					$shift = $this->GSUBsubstitute($ptr, $substitute, $Type, $GlyphPos); // GlyphPos contains positions to set null
1797					if ($this->debugOTL && $shift) {
1798						$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1799					}
1800					if ($shift) {
1801						return ($spos - $ptr + 1 - ($CompCount - 1));
1802					}
1803				}
1804			}
1805			return 0;
1806		} ////////////////////////////////////////////////////////////////////////////////
1807		// LookupType 5: Contextual Substitution Subtable
1808		////////////////////////////////////////////////////////////////////////////////
1809		elseif ($Type == 5) {
1810			//===========
1811			// Format 1: Simple Context Glyph Substitution
1812			//===========
1813			if ($SubstFormat == 1) {
1814				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
1815				$SubRuleSetCount = $this->read_ushort();
1816				$SubRuleSetOffset = [];
1817				for ($b = 0; $b < $SubRuleSetCount; $b++) {
1818					$offset = $this->read_ushort();
1819					if ($offset == 0x0000) {
1820						$SubRuleSetOffset[] = $offset;
1821					} else {
1822						$SubRuleSetOffset[] = $subtable_offset + $offset;
1823					}
1824				}
1825
1826				// SubRuleSet tables: All contexts beginning with the same glyph
1827				// Select the SubRuleSet required using the position of the glyph in the coverage table
1828				$GlyphPos = $LuCoverage[$currGID];
1829				if ($SubRuleSetOffset[$GlyphPos] > 0) {
1830					$this->seek($SubRuleSetOffset[$GlyphPos]);
1831					$SubRuleCnt = $this->read_ushort();
1832					$SubRule = [];
1833					for ($b = 0; $b < $SubRuleCnt; $b++) {
1834						$SubRule[$b] = $SubRuleSetOffset[$GlyphPos] + $this->read_ushort();
1835					}
1836					for ($b = 0; $b < $SubRuleCnt; $b++) {  // EACH RULE
1837						$this->seek($SubRule[$b]);
1838						$InputGlyphCount = $this->read_ushort();
1839						$SubstCount = $this->read_ushort();
1840
1841						$Backtrack = [];
1842						$Lookahead = [];
1843						$Input = [];
1844						$Input[0] = $this->OTLdata[$ptr]['uni'];
1845						for ($r = 1; $r < $InputGlyphCount; $r++) {
1846							$gid = $this->read_ushort();
1847							$Input[$r] = $this->glyphToChar($gid);
1848						}
1849						$matched = $this->checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr);
1850						if ($matched) {
1851							if ($this->debugOTL) {
1852								$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1853							}
1854							for ($p = 0; $p < $SubstCount; $p++) { // EACH LOOKUP
1855								$SequenceIndex[$p] = $this->read_ushort();
1856								$LookupListIndex[$p] = $this->read_ushort();
1857							}
1858
1859							for ($p = 0; $p < $SubstCount; $p++) {
1860								// Apply  $LookupListIndex  at   $SequenceIndex
1861								if ($SequenceIndex[$p] >= $InputGlyphCount) {
1862									continue;
1863								}
1864								$lu = $LookupListIndex[$p];
1865								$luType = $this->GSUBLookups[$lu]['Type'];
1866								$luFlag = $this->GSUBLookups[$lu]['Flag'];
1867								$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1868
1869								$luptr = $matched[$SequenceIndex[$p]];
1870								$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1871								$lucurrGID = $this->OTLdata[$luptr]['uni'];
1872
1873								foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
1874									$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);
1875									if ($shift) {
1876										break;
1877									}
1878								}
1879							}
1880
1881							if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
1882								return $shift;
1883							} /* OTL_FIX_3 */
1884							else {
1885								return $InputGlyphCount; // should be + matched ignores in Input Sequence
1886							}
1887						}
1888					}
1889				}
1890				return 0;
1891			} //===========
1892			// Format 2:
1893			//===========
1894			// Format 2: Class-based Context Glyph Substitution
1895			elseif ($SubstFormat == 2) {
1896				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
1897				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
1898				$SubClassSetCnt = $this->read_ushort();
1899				$SubClassSetOffset = [];
1900				for ($b = 0; $b < $SubClassSetCnt; $b++) {
1901					$offset = $this->read_ushort();
1902					if ($offset == 0x0000) {
1903						$SubClassSetOffset[] = $offset;
1904					} else {
1905						$SubClassSetOffset[] = $subtable_offset + $offset;
1906					}
1907				}
1908
1909				$InputClasses = $this->_getClasses($InputClassDefOffset);
1910
1911				for ($s = 0; $s < $SubClassSetCnt; $s++) { // $SubClassSet is ordered by input class-may be NULL
1912					// Select $SubClassSet if currGlyph is in First Input Class
1913					if ($SubClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
1914						$this->seek($SubClassSetOffset[$s]);
1915						$SubClassRuleCnt = $this->read_ushort();
1916						$SubClassRule = [];
1917						for ($b = 0; $b < $SubClassRuleCnt; $b++) {
1918							$SubClassRule[$b] = $SubClassSetOffset[$s] + $this->read_ushort();
1919						}
1920
1921						for ($b = 0; $b < $SubClassRuleCnt; $b++) {  // EACH RULE
1922							$this->seek($SubClassRule[$b]);
1923							$InputGlyphCount = $this->read_ushort();
1924							$SubstCount = $this->read_ushort();
1925							$Input = [];
1926							for ($r = 1; $r < $InputGlyphCount; $r++) {
1927								$Input[$r] = $this->read_ushort();
1928							}
1929
1930							$inputClass = $s;
1931
1932							$inputGlyphs = [];
1933							$inputGlyphs[0] = $InputClasses[$inputClass];
1934
1935							if ($InputGlyphCount > 1) {
1936								//  NB starts at 1
1937								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
1938									$classindex = $Input[$gcl];
1939									if (isset($InputClasses[$classindex])) {
1940										$inputGlyphs[$gcl] = $InputClasses[$classindex];
1941									} else {
1942										$inputGlyphs[$gcl] = '';
1943									}
1944								}
1945							}
1946
1947							// Class 0 contains all the glyphs NOT in the other classes
1948							$class0excl = [];
1949							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
1950								if (is_array($InputClasses[$gc])) {
1951									$class0excl = $class0excl + $InputClasses[$gc];
1952								}
1953							}
1954
1955							$backtrackGlyphs = [];
1956							$lookaheadGlyphs = [];
1957
1958							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl);
1959							if ($matched) {
1960								if ($this->debugOTL) {
1961									$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
1962								}
1963								for ($p = 0; $p < $SubstCount; $p++) { // EACH LOOKUP
1964									$SequenceIndex[$p] = $this->read_ushort();
1965									$LookupListIndex[$p] = $this->read_ushort();
1966								}
1967
1968								for ($p = 0; $p < $SubstCount; $p++) {
1969									// Apply  $LookupListIndex  at   $SequenceIndex
1970									if ($SequenceIndex[$p] >= $InputGlyphCount) {
1971										continue;
1972									}
1973									$lu = $LookupListIndex[$p];
1974									$luType = $this->GSUBLookups[$lu]['Type'];
1975									$luFlag = $this->GSUBLookups[$lu]['Flag'];
1976									$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
1977
1978									$luptr = $matched[$SequenceIndex[$p]];
1979									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
1980									$lucurrGID = $this->OTLdata[$luptr]['uni'];
1981
1982									foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
1983										$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);
1984										if ($shift) {
1985											break;
1986										}
1987									}
1988								}
1989
1990								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
1991									return $shift;
1992								} /* OTL_FIX_3 */
1993								else {
1994									return $InputGlyphCount; // should be + matched ignores in Input Sequence
1995								}
1996							}
1997						}
1998					}
1999				}
2000
2001				return 0;
2002			} //===========
2003			// Format 3:
2004			//===========
2005			// Format 3: Coverage-based Context Glyph Substitution
2006			elseif ($SubstFormat == 3) {
2007				throw new \Mpdf\MpdfException("GSUB Lookup Type " . $Type . " Format " . $SubstFormat . " not TESTED YET.");
2008			}
2009		} ////////////////////////////////////////////////////////////////////////////////
2010		// LookupType 6: Chaining Contextual Substitution Subtable
2011		////////////////////////////////////////////////////////////////////////////////
2012		elseif ($Type == 6) {
2013			//===========
2014			// Format 1:
2015			//===========
2016			// Format 1: Simple Chaining Context Glyph Substitution
2017			if ($SubstFormat == 1) {
2018				$Coverage = $subtable_offset + $this->read_ushort();
2019				$GlyphPos = $LuCoverage[$currGID];
2020				$ChainSubRuleSetCount = $this->read_ushort();
2021				// All of the ChainSubRule tables defining contexts that begin with the same first glyph are grouped together and defined in a ChainSubRuleSet table
2022				$this->skip($GlyphPos * 2);
2023				$ChainSubRuleSet = $subtable_offset + $this->read_ushort();
2024				$this->seek($ChainSubRuleSet);
2025				$ChainSubRuleCount = $this->read_ushort();
2026
2027				for ($s = 0; $s < $ChainSubRuleCount; $s++) {
2028					$ChainSubRule[$s] = $ChainSubRuleSet + $this->read_ushort();
2029				}
2030
2031				for ($s = 0; $s < $ChainSubRuleCount; $s++) {
2032					$this->seek($ChainSubRule[$s]);
2033
2034					$BacktrackGlyphCount = $this->read_ushort();
2035					$Backtrack = [];
2036					for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
2037						$gid = $this->read_ushort();
2038						$Backtrack[] = $this->glyphToChar($gid);
2039					}
2040					$Input = [];
2041					$Input[0] = $this->OTLdata[$ptr]['uni'];
2042					$InputGlyphCount = $this->read_ushort();
2043					for ($b = 1; $b < $InputGlyphCount; $b++) {
2044						$gid = $this->read_ushort();
2045						$Input[$b] = $this->glyphToChar($gid);
2046					}
2047					$LookaheadGlyphCount = $this->read_ushort();
2048					$Lookahead = [];
2049					for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
2050						$gid = $this->read_ushort();
2051						$Lookahead[] = $this->glyphToChar($gid);
2052					}
2053
2054					$matched = $this->checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr);
2055					if ($matched) {
2056						if ($this->debugOTL) {
2057							$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
2058						}
2059						$SubstCount = $this->read_ushort();
2060						for ($p = 0; $p < $SubstCount; $p++) {
2061							// SubstLookupRecord
2062							$SubstLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
2063							$SubstLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
2064						}
2065						for ($p = 0; $p < $SubstCount; $p++) {
2066							// Apply  $SubstLookupRecord[$p]['LookupListIndex']  at   $SubstLookupRecord[$p]['SequenceIndex']
2067							if ($SubstLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) {
2068								continue;
2069							}
2070							$lu = $SubstLookupRecord[$p]['LookupListIndex'];
2071							$luType = $this->GSUBLookups[$lu]['Type'];
2072							$luFlag = $this->GSUBLookups[$lu]['Flag'];
2073							$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2074
2075							$luptr = $matched[$SubstLookupRecord[$p]['SequenceIndex']];
2076							$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2077							$lucurrGID = $this->OTLdata[$luptr]['uni'];
2078
2079							foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
2080								$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);
2081								if ($shift) {
2082									break;
2083								}
2084							}
2085						}
2086						if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2087							return $shift;
2088						} /* OTL_FIX_3 */
2089						else {
2090							return $InputGlyphCount; // should be + matched ignores in Input Sequence
2091						}
2092					}
2093				}
2094				return 0;
2095			} //===========
2096			// Format 2:
2097			//===========
2098			// Format 2: Class-based Chaining Context Glyph Substitution  p257
2099			elseif ($SubstFormat == 2) {
2100				// 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)
2101
2102				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
2103				$BacktrackClassDefOffset = $subtable_offset + $this->read_ushort();
2104				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
2105				$LookaheadClassDefOffset = $subtable_offset + $this->read_ushort();
2106				$ChainSubClassSetCnt = $this->read_ushort();
2107				$ChainSubClassSetOffset = [];
2108				for ($b = 0; $b < $ChainSubClassSetCnt; $b++) {
2109					$offset = $this->read_ushort();
2110					if ($offset == 0x0000) {
2111						$ChainSubClassSetOffset[] = $offset;
2112					} else {
2113						$ChainSubClassSetOffset[] = $subtable_offset + $offset;
2114					}
2115				}
2116
2117				$BacktrackClasses = $this->_getClasses($BacktrackClassDefOffset);
2118				$InputClasses = $this->_getClasses($InputClassDefOffset);
2119				$LookaheadClasses = $this->_getClasses($LookaheadClassDefOffset);
2120
2121				for ($s = 0; $s < $ChainSubClassSetCnt; $s++) { // $ChainSubClassSet is ordered by input class-may be NULL
2122					// Select $ChainSubClassSet if currGlyph is in First Input Class
2123					if ($ChainSubClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
2124						$this->seek($ChainSubClassSetOffset[$s]);
2125						$ChainSubClassRuleCnt = $this->read_ushort();
2126						$ChainSubClassRule = [];
2127						for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {
2128							$ChainSubClassRule[$b] = $ChainSubClassSetOffset[$s] + $this->read_ushort();
2129						}
2130
2131						for ($b = 0; $b < $ChainSubClassRuleCnt; $b++) {  // EACH RULE
2132							$this->seek($ChainSubClassRule[$b]);
2133							$BacktrackGlyphCount = $this->read_ushort();
2134							for ($r = 0; $r < $BacktrackGlyphCount; $r++) {
2135								$Backtrack[$r] = $this->read_ushort();
2136							}
2137							$InputGlyphCount = $this->read_ushort();
2138							for ($r = 1; $r < $InputGlyphCount; $r++) {
2139								$Input[$r] = $this->read_ushort();
2140							}
2141							$LookaheadGlyphCount = $this->read_ushort();
2142							for ($r = 0; $r < $LookaheadGlyphCount; $r++) {
2143								$Lookahead[$r] = $this->read_ushort();
2144							}
2145
2146
2147							// These contain classes of glyphs as arrays
2148							// $InputClasses[(class)] e.g. 0x02E6,0x02E7,0x02E8
2149							// $LookaheadClasses[(class)]
2150							// $BacktrackClasses[(class)]
2151							// These contain arrays of classIndexes
2152							// [Backtrack] [Lookahead] and [Input] (Input is from the second position only)
2153
2154
2155							$inputClass = $s; //???
2156
2157							$inputGlyphs = [];
2158							$inputGlyphs[0] = $InputClasses[$inputClass];
2159
2160							if ($InputGlyphCount > 1) {
2161								//  NB starts at 1
2162								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
2163									$classindex = $Input[$gcl];
2164									if (isset($InputClasses[$classindex])) {
2165										$inputGlyphs[$gcl] = $InputClasses[$classindex];
2166									} else {
2167										$inputGlyphs[$gcl] = '';
2168									}
2169								}
2170							}
2171
2172							// Class 0 contains all the glyphs NOT in the other classes
2173							$class0excl = [];
2174							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
2175								if (isset($InputClasses[$gc])) {
2176									$class0excl = $class0excl + $InputClasses[$gc];
2177								}
2178							}
2179
2180							if ($BacktrackGlyphCount) {
2181								for ($gcl = 0; $gcl < $BacktrackGlyphCount; $gcl++) {
2182									$classindex = $Backtrack[$gcl];
2183									if (isset($BacktrackClasses[$classindex])) {
2184										$backtrackGlyphs[$gcl] = $BacktrackClasses[$classindex];
2185									} else {
2186										$backtrackGlyphs[$gcl] = '';
2187									}
2188								}
2189							} else {
2190								$backtrackGlyphs = [];
2191							}
2192
2193							// Class 0 contains all the glyphs NOT in the other classes
2194							$bclass0excl = [];
2195							for ($gc = 1; $gc <= count($BacktrackClasses); $gc++) {
2196								if (isset($BacktrackClasses[$gc])) {
2197									$bclass0excl = $bclass0excl + $BacktrackClasses[$gc];
2198								}
2199							}
2200
2201
2202							if ($LookaheadGlyphCount) {
2203								for ($gcl = 0; $gcl < $LookaheadGlyphCount; $gcl++) {
2204									$classindex = $Lookahead[$gcl];
2205									if (isset($LookaheadClasses[$classindex])) {
2206										$lookaheadGlyphs[$gcl] = $LookaheadClasses[$classindex];
2207									} else {
2208										$lookaheadGlyphs[$gcl] = '';
2209									}
2210								}
2211							} else {
2212								$lookaheadGlyphs = [];
2213							}
2214
2215							// Class 0 contains all the glyphs NOT in the other classes
2216							$lclass0excl = [];
2217							for ($gc = 1; $gc <= count($LookaheadClasses); $gc++) {
2218								if (isset($LookaheadClasses[$gc])) {
2219									$lclass0excl = $lclass0excl + $LookaheadClasses[$gc];
2220								}
2221							}
2222
2223
2224							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl, $bclass0excl, $lclass0excl);
2225							if ($matched) {
2226								if ($this->debugOTL) {
2227									$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
2228								}
2229								$SubstCount = $this->read_ushort();
2230								for ($p = 0; $p < $SubstCount; $p++) { // EACH LOOKUP
2231									$SequenceIndex[$p] = $this->read_ushort();
2232									$LookupListIndex[$p] = $this->read_ushort();
2233								}
2234
2235								for ($p = 0; $p < $SubstCount; $p++) {
2236									// Apply  $LookupListIndex  at   $SequenceIndex
2237									if ($SequenceIndex[$p] >= $InputGlyphCount) {
2238										continue;
2239									}
2240									$lu = $LookupListIndex[$p];
2241									$luType = $this->GSUBLookups[$lu]['Type'];
2242									$luFlag = $this->GSUBLookups[$lu]['Flag'];
2243									$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2244
2245									$luptr = $matched[$SequenceIndex[$p]];
2246									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2247									$lucurrGID = $this->OTLdata[$luptr]['uni'];
2248
2249									foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
2250										$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);
2251										if ($shift) {
2252											break;
2253										}
2254									}
2255								}
2256
2257								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2258									return $shift;
2259								} /* OTL_FIX_3 */
2260								else {
2261									return $InputGlyphCount; // should be + matched ignores in Input Sequence
2262								}
2263							}
2264						}
2265					}
2266				}
2267
2268				return 0;
2269			} //===========
2270			// Format 3:
2271			//===========
2272			// Format 3: Coverage-based Chaining Context Glyph Substitution  p259
2273			elseif ($SubstFormat == 3) {
2274				$BacktrackGlyphCount = $this->read_ushort();
2275				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
2276					$CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2277				}
2278				$InputGlyphCount = $this->read_ushort();
2279				for ($b = 0; $b < $InputGlyphCount; $b++) {
2280					$CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2281				}
2282				$LookaheadGlyphCount = $this->read_ushort();
2283				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
2284					$CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
2285				}
2286				$SubstCount = $this->read_ushort();
2287				$save_pos = $this->_pos; // Save the point just after PosCount
2288
2289				$CoverageBacktrackGlyphs = [];
2290				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
2291					$this->seek($CoverageBacktrackOffset[$b]);
2292					$glyphs = $this->_getCoverage();
2293					$CoverageBacktrackGlyphs[$b] = implode("|", $glyphs);
2294				}
2295				$CoverageInputGlyphs = [];
2296				for ($b = 0; $b < $InputGlyphCount; $b++) {
2297					$this->seek($CoverageInputOffset[$b]);
2298					$glyphs = $this->_getCoverage();
2299					$CoverageInputGlyphs[$b] = implode("|", $glyphs);
2300				}
2301				$CoverageLookaheadGlyphs = [];
2302				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
2303					$this->seek($CoverageLookaheadOffset[$b]);
2304					$glyphs = $this->_getCoverage();
2305					$CoverageLookaheadGlyphs[$b] = implode("|", $glyphs);
2306				}
2307
2308				$matched = $this->checkContextMatchMultiple($CoverageInputGlyphs, $CoverageBacktrackGlyphs, $CoverageLookaheadGlyphs, $ignore, $ptr);
2309				if ($matched) {
2310					if ($this->debugOTL) {
2311						$this->_dumpproc('GSUB', $lookupID, $subtable, $Type, $SubstFormat, $ptr, $currGlyph, $level);
2312					}
2313
2314					$this->seek($save_pos); // Return to just after PosCount
2315					for ($p = 0; $p < $SubstCount; $p++) {
2316						// SubstLookupRecord
2317						$SubstLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
2318						$SubstLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
2319					}
2320					for ($p = 0; $p < $SubstCount; $p++) {
2321						// Apply  $SubstLookupRecord[$p]['LookupListIndex']  at   $SubstLookupRecord[$p]['SequenceIndex']
2322						if ($SubstLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) {
2323							continue;
2324						}
2325						$lu = $SubstLookupRecord[$p]['LookupListIndex'];
2326						$luType = $this->GSUBLookups[$lu]['Type'];
2327						$luFlag = $this->GSUBLookups[$lu]['Flag'];
2328						$luMarkFilteringSet = $this->GSUBLookups[$lu]['MarkFilteringSet'];
2329
2330						$luptr = $matched[$SubstLookupRecord[$p]['SequenceIndex']];
2331						$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
2332						$lucurrGID = $this->OTLdata[$luptr]['uni'];
2333
2334						foreach ($this->GSUBLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
2335							$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);
2336							if ($shift) {
2337								break;
2338							}
2339						}
2340					}
2341					if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
2342						return (isset($shift) ? $shift : 0);
2343					} /* OTL_FIX_3 */
2344					else {
2345						return $InputGlyphCount; // should be + matched ignores in Input Sequence
2346					}
2347				}
2348
2349				return 0;
2350			}
2351		} else {
2352			throw new \Mpdf\MpdfException("GSUB Lookup Type " . $Type . " not supported.");
2353		}
2354	}
2355
2356	function _updateLigatureMarks($pos, $n)
2357	{
2358		if ($n > 0) {
2359			// Update position of Ligatures and associated Marks
2360			// Foreach lig/assocMarks
2361			// Any position lpos or mpos > $pos + count($substitute)
2362			//  $this->assocMarks = array();    // assocMarks[$pos mpos] => array(compID, ligPos)
2363			//  $this->assocLigs = array(); // Ligatures[$pos lpos] => nc
2364			for ($p = count($this->OTLdata) - 1; $p >= ($pos + $n); $p--) {
2365				if (isset($this->assocLigs[$p])) {
2366					$tmp = $this->assocLigs[$p];
2367					unset($this->assocLigs[$p]);
2368					$this->assocLigs[($p + $n)] = $tmp;
2369				}
2370			}
2371			for ($p = count($this->OTLdata) - 1; $p >= 0; $p--) {
2372				if (isset($this->assocMarks[$p])) {
2373					if ($this->assocMarks[$p]['ligPos'] >= ($pos + $n)) {
2374						$this->assocMarks[$p]['ligPos'] += $n;
2375					}
2376					if ($p >= ($pos + $n)) {
2377						$tmp = $this->assocMarks[$p];
2378						unset($this->assocMarks[$p]);
2379						$this->assocMarks[($p + $n)] = $tmp;
2380					}
2381				}
2382			}
2383		} elseif ($n < 1) { // glyphs removed
2384			$nrem = -$n;
2385			// Update position of pre-existing Ligatures and associated Marks
2386			for ($p = ($pos + 1); $p < count($this->OTLdata); $p++) {
2387				if (isset($this->assocLigs[$p])) {
2388					$tmp = $this->assocLigs[$p];
2389					unset($this->assocLigs[$p]);
2390					$this->assocLigs[($p - $nrem)] = $tmp;
2391				}
2392			}
2393			for ($p = 0; $p < count($this->OTLdata); $p++) {
2394				if (isset($this->assocMarks[$p])) {
2395					if ($this->assocMarks[$p]['ligPos'] >= ($pos)) {
2396						$this->assocMarks[$p]['ligPos'] -= $nrem;
2397					}
2398					if ($p > $pos) {
2399						$tmp = $this->assocMarks[$p];
2400						unset($this->assocMarks[$p]);
2401						$this->assocMarks[($p - $nrem)] = $tmp;
2402					}
2403				}
2404			}
2405		}
2406	}
2407
2408	function GSUBsubstitute($pos, $substitute, $Type, $GlyphPos = null)
2409	{
2410
2411		// LookupType 1: Simple Substitution Subtable : 1 to 1
2412		// LookupType 3: Alternate Forms : 1 to 1(n)
2413		if ($Type == 1 || $Type == 3) {
2414			$this->OTLdata[$pos]['uni'] = $substitute;
2415			$this->OTLdata[$pos]['hex'] = $this->unicode_hex($substitute);
2416			return 1;
2417		} // LookupType 2: Multiple Substitution Subtable : 1 to n
2418		elseif ($Type == 2) {
2419			for ($i = 0; $i < count($substitute); $i++) {
2420				$uni = $substitute[$i];
2421				$newOTLdata[$i] = [];
2422				$newOTLdata[$i]['uni'] = $uni;
2423				$newOTLdata[$i]['hex'] = $this->unicode_hex($uni);
2424
2425
2426				// Get types of new inserted chars - or replicate type of char being replaced
2427				//  $bt = Ucdn::get_bidi_class($uni);
2428				//  if (!$bt) {
2429				$bt = $this->OTLdata[$pos]['bidi_type'];
2430				//  }
2431
2432				if (strpos($this->GlyphClassMarks, $newOTLdata[$i]['hex']) !== false) {
2433					$gp = 'M';
2434				} elseif ($uni == 32) {
2435					$gp = 'S';
2436				} else {
2437					$gp = 'C';
2438				}
2439
2440				// Need to update matra_type ??? of new glyphs inserted ???????????????????????????????????????
2441
2442				$newOTLdata[$i]['bidi_type'] = $bt;
2443				$newOTLdata[$i]['group'] = $gp;
2444
2445				// Need to update details of new glyphs inserted
2446				$newOTLdata[$i]['general_category'] = $this->OTLdata[$pos]['general_category'];
2447
2448				if ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
2449					$newOTLdata[$i]['indic_category'] = $this->OTLdata[$pos]['indic_category'];
2450					$newOTLdata[$i]['indic_position'] = $this->OTLdata[$pos]['indic_position'];
2451				} elseif ($this->shaper == 'M') {
2452					$newOTLdata[$i]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category'];
2453					$newOTLdata[$i]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position'];
2454				}
2455				if (isset($this->OTLdata[$pos]['mask'])) {
2456					$newOTLdata[$i]['mask'] = $this->OTLdata[$pos]['mask'];
2457				}
2458				if (isset($this->OTLdata[$pos]['syllable'])) {
2459					$newOTLdata[$i]['syllable'] = $this->OTLdata[$pos]['syllable'];
2460				}
2461			}
2462			if ($this->shaper == 'K' || $this->shaper == 'T' || $this->shaper == 'L') {
2463				if ($this->OTLdata[$pos]['wordend']) {
2464					$newOTLdata[count($substitute) - 1]['wordend'] = true;
2465				}
2466			}
2467
2468			array_splice($this->OTLdata, $pos, 1, $newOTLdata); // Replace 1 with n
2469			// Update position of Ligatures and associated Marks
2470			// count($substitute)-1  is the number of glyphs added
2471			$nadd = count($substitute) - 1;
2472			$this->_updateLigatureMarks($pos, $nadd);
2473			return count($substitute);
2474		} // LookupType 4: Ligature Substitution Subtable : n to 1
2475		elseif ($Type == 4) {
2476			// Create Ligatures and associated Marks
2477			$firstGlyph = $this->OTLdata[$pos]['hex'];
2478
2479			// If all components of the ligature are marks (and in the same syllable), we call this a mark ligature.
2480			$contains_marks = false;
2481			$contains_nonmarks = false;
2482			if (isset($this->OTLdata[$pos]['syllable'])) {
2483				$current_syllable = $this->OTLdata[$pos]['syllable'];
2484			} else {
2485				$current_syllable = 0;
2486			}
2487			for ($i = 0; $i < count($GlyphPos); $i++) {
2488				// If subsequent components are not Marks as well - don't ligate
2489				$unistr = $this->OTLdata[$GlyphPos[$i]]['hex'];
2490				if ($this->restrictToSyllable && isset($this->OTLdata[$GlyphPos[$i]]['syllable']) && $this->OTLdata[$GlyphPos[$i]]['syllable'] != $current_syllable) {
2491					return 0;
2492				}
2493				if (strpos($this->GlyphClassMarks, $unistr) !== false) {
2494					$contains_marks = true;
2495				} else {
2496					$contains_nonmarks = true;
2497				}
2498			}
2499			if ($contains_marks && !$contains_nonmarks) {
2500				// Mark Ligature (all components are Marks)
2501				$firstMarkAssoc = '';
2502				if (isset($this->assocMarks[$pos])) {
2503					$firstMarkAssoc = $this->assocMarks[$pos];
2504				}
2505				// If all components of the ligature are marks, we call this a mark ligature.
2506				for ($i = 1; $i < count($GlyphPos); $i++) {
2507					// If subsequent components are not Marks as well - don't ligate
2508					//      $unistr = $this->OTLdata[$GlyphPos[$i]]['hex'];
2509					//      if (strpos($this->GlyphClassMarks, $unistr )===false) { return; }
2510
2511					$nextMarkAssoc = '';
2512					if (isset($this->assocMarks[$GlyphPos[$i]])) {
2513						$nextMarkAssoc = $this->assocMarks[$GlyphPos[$i]];
2514					}
2515					// If first component was attached to a previous ligature component,
2516					// all subsequent components should be attached to the same ligature
2517					// component, otherwise we shouldn't ligate them.
2518					// If first component was NOT attached to a previous ligature component,
2519					// all subsequent components should also NOT be attached to any ligature component,
2520					if ($firstMarkAssoc != $nextMarkAssoc) {
2521						// unless they are attached to the first component itself!
2522						//          if (!is_array($nextMarkAssoc) || $nextMarkAssoc['ligPos']!= $pos) { return; }
2523						// Update/Edit - In test with myanmartext font
2524						// &#x1004;&#x103a;&#x1039;&#x1000;&#x1039;&#x1000;&#x103b;&#x103c;&#x103d;&#x1031;&#x102d;
2525						// => Lookup 17  E003 E066B E05A 102D
2526						// E003 and 102D should form a mark ligature, but 102D is already associated with (non-mark) ligature E05A
2527						// So instead of disallowing the mark ligature to form, just dissociate...
2528						if (!is_array($nextMarkAssoc) || $nextMarkAssoc['ligPos'] != $pos) {
2529							unset($this->assocMarks[$GlyphPos[$i]]);
2530						}
2531					}
2532				}
2533
2534				/*
2535				 * - If it *is* a mark ligature, we don't allocate a new ligature id, and leave
2536				 *   the ligature to keep its old ligature id.  This will allow it to attach to
2537				 *   a base ligature in GPOS.  Eg. if the sequence is: LAM,LAM,SHADDA,FATHA,HEH,
2538				 *   and LAM,LAM,HEH form a ligature, they will leave SHADDA and FATHA wit a
2539				 *   ligature id and component value of 2.  Then if SHADDA,FATHA form a ligature
2540				 *   later, we don't want them to lose their ligature id/component, otherwise
2541				 *   GPOS will fail to correctly position the mark ligature on top of the
2542				 *   LAM,LAM,HEH ligature.
2543				 */
2544				// So if is_array($firstMarkAssoc) - the new (Mark) ligature should keep this association
2545
2546				$lastPos = $GlyphPos[(count($GlyphPos) - 1)];
2547			} else {
2548				/*
2549				 * - Ligatures cannot be formed across glyphs attached to different components
2550				 *   of previous ligatures.  Eg. the sequence is LAM,SHADDA,LAM,FATHA,HEH, and
2551				 *   LAM,LAM,HEH form a ligature, leaving SHADDA,FATHA next to eachother.
2552				 *   However, it would be wrong to ligate that SHADDA,FATHA sequence.
2553				 *   There is an exception to this: If a ligature tries ligating with marks that
2554				 *   belong to it itself, go ahead, assuming that the font designer knows what
2555				 *   they are doing (otherwise it can break Indic stuff when a matra wants to
2556				 *   ligate with a conjunct...)
2557				 */
2558
2559				/*
2560				 * - If a ligature is formed of components that some of which are also ligatures
2561				 *   themselves, and those ligature components had marks attached to *their*
2562				 *   components, we have to attach the marks to the new ligature component
2563				 *   positions!  Now *that*'s tricky!  And these marks may be following the
2564				 *   last component of the whole sequence, so we should loop forward looking
2565				 *   for them and update them.
2566				 *
2567				 *   Eg. the sequence is LAM,LAM,SHADDA,FATHA,HEH, and the font first forms a
2568				 *   'calt' ligature of LAM,HEH, leaving the SHADDA and FATHA with a ligature
2569				 *   id and component == 1.  Now, during 'liga', the LAM and the LAM-HEH ligature
2570				 *   form a LAM-LAM-HEH ligature.  We need to reassign the SHADDA and FATHA to
2571				 *   the new ligature with a component value of 2.
2572				 *
2573				 *   This in fact happened to a font...  See:
2574				 *   https://bugzilla.gnome.org/show_bug.cgi?id=437633
2575				 */
2576
2577				$currComp = 0;
2578				for ($i = 0; $i < count($GlyphPos); $i++) {
2579					if ($i > 0 && isset($this->assocLigs[$GlyphPos[$i]])) { // One of the other components is already a ligature
2580						$nc = $this->assocLigs[$GlyphPos[$i]];
2581					} else {
2582						$nc = 1;
2583					}
2584					// While next char to right is a mark (but not the next matched glyph)
2585					// ?? + also include a Mark Ligature here
2586					$ic = 1;
2587					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) {
2588						$newComp = $currComp;
2589						if (isset($this->assocMarks[$GlyphPos[$i] + $ic])) { // One of the inbetween Marks is already associated with a Lig
2590							// OK as long as it is associated with the current Lig
2591							//      if ($this->assocMarks[($GlyphPos[$i]+$ic)]['ligPos'] != ($GlyphPos[$i]+$ic)) { die("Problem #1"); }
2592							$newComp += $this->assocMarks[($GlyphPos[$i] + $ic)]['compID'];
2593						}
2594						$this->assocMarks[($GlyphPos[$i] + $ic)] = ['compID' => $newComp, 'ligPos' => $pos];
2595						$ic++;
2596					}
2597					$currComp += $nc;
2598				}
2599				$lastPos = $GlyphPos[(count($GlyphPos) - 1)] + $ic - 1;
2600				$this->assocLigs[$pos] = $currComp; // Number of components in new Ligature
2601			}
2602
2603			// Now remove the unwanted glyphs and associated metadata
2604			$newOTLdata[0] = [];
2605
2606			// Get types of new inserted chars - or replicate type of char being replaced
2607			//  $bt = Ucdn::get_bidi_class($substitute);
2608			//  if (!$bt) {
2609			$bt = $this->OTLdata[$pos]['bidi_type'];
2610			//  }
2611
2612			if (strpos($this->GlyphClassMarks, $this->unicode_hex($substitute)) !== false) {
2613				$gp = 'M';
2614			} elseif ($substitute == 32) {
2615				$gp = 'S';
2616			} else {
2617				$gp = 'C';
2618			}
2619
2620			// Need to update details of new glyphs inserted
2621			$newOTLdata[0]['general_category'] = $this->OTLdata[$pos]['general_category'];
2622
2623			$newOTLdata[0]['bidi_type'] = $bt;
2624			$newOTLdata[0]['group'] = $gp;
2625
2626			// KASHIDA: If forming a ligature when the last component was identified as a kashida point (final form)
2627			// If previous/first component of ligature is a medial form, then keep this as a kashida point
2628			// TEST (Arabic Typesetting) &#x64a;&#x64e;&#x646;&#x62a;&#x64f;&#x645;
2629			$ka = 0;
2630			if (isset($this->OTLdata[$GlyphPos[(count($GlyphPos) - 1)]]['GPOSinfo']['kashida'])) {
2631				$ka = $this->OTLdata[$GlyphPos[(count($GlyphPos) - 1)]]['GPOSinfo']['kashida'];
2632			}
2633			if ($ka == 1 && isset($this->OTLdata[$pos]['form']) && $this->OTLdata[$pos]['form'] == 3) {
2634				$newOTLdata[0]['GPOSinfo']['kashida'] = $ka;
2635			}
2636
2637			$newOTLdata[0]['uni'] = $substitute;
2638			$newOTLdata[0]['hex'] = $this->unicode_hex($substitute);
2639
2640			if ($this->shaper == 'I' || $this->shaper == 'K' || $this->shaper == 'S') {
2641				$newOTLdata[0]['indic_category'] = $this->OTLdata[$pos]['indic_category'];
2642				$newOTLdata[0]['indic_position'] = $this->OTLdata[$pos]['indic_position'];
2643			} elseif ($this->shaper == 'M') {
2644				$newOTLdata[0]['myanmar_category'] = $this->OTLdata[$pos]['myanmar_category'];
2645				$newOTLdata[0]['myanmar_position'] = $this->OTLdata[$pos]['myanmar_position'];
2646			}
2647			if (isset($this->OTLdata[$pos]['mask'])) {
2648				$newOTLdata[0]['mask'] = $this->OTLdata[$pos]['mask'];
2649			}
2650			if (isset($this->OTLdata[$pos]['syllable'])) {
2651				$newOTLdata[0]['syllable'] = $this->OTLdata[$pos]['syllable'];
2652			}
2653
2654			$newOTLdata[0]['is_ligature'] = true;
2655
2656
2657			array_splice($this->OTLdata, $pos, 1, $newOTLdata);
2658
2659			// GlyphPos contains array of arr_pos to set null - not necessarily contiguous
2660			// +- Remove any assocMarks or assocLigs from the main components (the ones that are deleted)
2661			for ($i = count($GlyphPos) - 1; $i > 0; $i--) {
2662				$gpos = $GlyphPos[$i];
2663				array_splice($this->OTLdata, $gpos, 1);
2664				unset($this->assocLigs[$gpos]);
2665				unset($this->assocMarks[$gpos]);
2666			}
2667			//  $this->assocLigs = array(); // Ligatures[$posarr lpos] => nc
2668			//  $this->assocMarks = array();    // assocMarks[$posarr mpos] => array(compID, ligPos)
2669			// Update position of pre-existing Ligatures and associated Marks
2670			// Start after first GlyphPos
2671			// count($GlyphPos)-1  is the number of glyphs removed from string
2672			for ($p = ($GlyphPos[0] + 1); $p < (count($this->OTLdata) + count($GlyphPos) - 1); $p++) {
2673				$nrem = 0; // Number of Glyphs removed at this point in the string
2674				for ($i = 0; $i < count($GlyphPos); $i++) {
2675					if ($i > 0 && $p > $GlyphPos[$i]) {
2676						$nrem++;
2677					}
2678				}
2679				if (isset($this->assocLigs[$p])) {
2680					$tmp = $this->assocLigs[$p];
2681					unset($this->assocLigs[$p]);
2682					$this->assocLigs[($p - $nrem)] = $tmp;
2683				}
2684				if (isset($this->assocMarks[$p])) {
2685					$tmp = $this->assocMarks[$p];
2686					unset($this->assocMarks[$p]);
2687					if ($tmp['ligPos'] > $GlyphPos[0]) {
2688						$tmp['ligPos'] -= $nrem;
2689					}
2690					$this->assocMarks[($p - $nrem)] = $tmp;
2691				}
2692			}
2693			return 1;
2694		} else {
2695			return 0;
2696		}
2697	}
2698
2699	////////////////////////////////////////////////////////////////
2700	//////////       ARABIC        /////////////////////////////////
2701	////////////////////////////////////////////////////////////////
2702	private function arabic_initialise()
2703	{
2704		// cf. http://unicode.org/Public/UNIDATA/ArabicShaping.txt
2705		// http://unicode.org/Public/UNIDATA/extracted/DerivedJoiningType.txt
2706		// JOIN TO FOLLOWING LETTER IN LOGICAL ORDER (i.e. AS INITIAL/MEDIAL FORM) = Unicode Left-Joining (+ Dual-Joining + Join_Causing 00640)
2707		$this->arabLeftJoining = [
2708			0x0620 => 1, 0x0626 => 1, 0x0628 => 1, 0x062A => 1, 0x062B => 1, 0x062C => 1, 0x062D => 1, 0x062E => 1,
2709			0x0633 => 1, 0x0634 => 1, 0x0635 => 1, 0x0636 => 1, 0x0637 => 1, 0x0638 => 1, 0x0639 => 1, 0x063A => 1,
2710			0x063B => 1, 0x063C => 1, 0x063D => 1, 0x063E => 1, 0x063F => 1, 0x0640 => 1, 0x0641 => 1, 0x0642 => 1,
2711			0x0643 => 1, 0x0644 => 1, 0x0645 => 1, 0x0646 => 1, 0x0647 => 1, 0x0649 => 1, 0x064A => 1, 0x066E => 1,
2712			0x066F => 1, 0x0678 => 1, 0x0679 => 1, 0x067A => 1, 0x067B => 1, 0x067C => 1, 0x067D => 1, 0x067E => 1,
2713			0x067F => 1, 0x0680 => 1, 0x0681 => 1, 0x0682 => 1, 0x0683 => 1, 0x0684 => 1, 0x0685 => 1, 0x0686 => 1,
2714			0x0687 => 1, 0x069A => 1, 0x069B => 1, 0x069C => 1, 0x069D => 1, 0x069E => 1, 0x069F => 1, 0x06A0 => 1,
2715			0x06A1 => 1, 0x06A2 => 1, 0x06A3 => 1, 0x06A4 => 1, 0x06A5 => 1, 0x06A6 => 1, 0x06A7 => 1, 0x06A8 => 1,
2716			0x06A9 => 1, 0x06AA => 1, 0x06AB => 1, 0x06AC => 1, 0x06AD => 1, 0x06AE => 1, 0x06AF => 1, 0x06B0 => 1,
2717			0x06B1 => 1, 0x06B2 => 1, 0x06B3 => 1, 0x06B4 => 1, 0x06B5 => 1, 0x06B6 => 1, 0x06B7 => 1, 0x06B8 => 1,
2718			0x06B9 => 1, 0x06BA => 1, 0x06BB => 1, 0x06BC => 1, 0x06BD => 1, 0x06BE => 1, 0x06BF => 1, 0x06C1 => 1,
2719			0x06C2 => 1, 0x06CC => 1, 0x06CE => 1, 0x06D0 => 1, 0x06D1 => 1, 0x06FA => 1, 0x06FB => 1, 0x06FC => 1,
2720			0x06FF => 1,
2721			/* Arabic Supplement */
2722			0x0750 => 1, 0x0751 => 1, 0x0752 => 1, 0x0753 => 1, 0x0754 => 1, 0x0755 => 1, 0x0756 => 1, 0x0757 => 1,
2723			0x0758 => 1, 0x075C => 1, 0x075D => 1, 0x075E => 1, 0x075F => 1, 0x0760 => 1, 0x0761 => 1, 0x0762 => 1,
2724			0x0763 => 1, 0x0764 => 1, 0x0765 => 1, 0x0766 => 1, 0x0767 => 1, 0x0768 => 1, 0x0769 => 1, 0x076A => 1,
2725			0x076D => 1, 0x076E => 1, 0x076F => 1, 0x0770 => 1, 0x0772 => 1, 0x0775 => 1, 0x0776 => 1, 0x0777 => 1,
2726			0x077A => 1, 0x077B => 1, 0x077C => 1, 0x077D => 1, 0x077E => 1, 0x077F => 1,
2727			/* Extended Arabic */
2728			0x08A0 => 1, 0x08A2 => 1, 0x08A3 => 1, 0x08A4 => 1, 0x08A5 => 1, 0x08A6 => 1, 0x08A7 => 1, 0x08A8 => 1,
2729			0x08A9 => 1,
2730			/* 'syrc' Syriac */
2731			0x0712 => 1, 0x0713 => 1, 0x0714 => 1, 0x071A => 1, 0x071B => 1, 0x071C => 1, 0x071D => 1, 0x071F => 1,
2732			0x0720 => 1, 0x0721 => 1, 0x0722 => 1, 0x0723 => 1, 0x0724 => 1, 0x0725 => 1, 0x0726 => 1, 0x0727 => 1,
2733			0x0729 => 1, 0x072B => 1, 0x072D => 1, 0x072E => 1, 0x074E => 1, 0x074F => 1,
2734			/* N'Ko */
2735			0x07CA => 1, 0x07CB => 1, 0x07CC => 1, 0x07CD => 1, 0x07CE => 1, 0x07CF => 1, 0x07D0 => 1, 0x07D1 => 1,
2736			0x07D2 => 1, 0x07D3 => 1, 0x07D4 => 1, 0x07D5 => 1, 0x07D6 => 1, 0x07D7 => 1, 0x07D8 => 1, 0x07D9 => 1,
2737			0x07DA => 1, 0x07DB => 1, 0x07DC => 1, 0x07DD => 1, 0x07DE => 1, 0x07DF => 1, 0x07E0 => 1, 0x07E1 => 1,
2738			0x07E2 => 1, 0x07E3 => 1, 0x07E4 => 1, 0x07E5 => 1, 0x07E6 => 1, 0x07E7 => 1, 0x07E8 => 1, 0x07E9 => 1,
2739			0x07EA => 1, 0x07FA => 1,
2740			/* Mandaic */
2741			0x0841 => 1, 0x0842 => 1, 0x0843 => 1, 0x0844 => 1, 0x0845 => 1, 0x0847 => 1, 0x0848 => 1, 0x084A => 1,
2742			0x084B => 1, 0x084C => 1, 0x084D => 1, 0x084E => 1, 0x0850 => 1, 0x0851 => 1, 0x0852 => 1, 0x0853 => 1,
2743			0x0855 => 1,
2744			/* ZWJ U+200D */
2745			0x0200D => 1];
2746
2747		/* JOIN TO PREVIOUS LETTER IN LOGICAL ORDER (i.e. AS FINAL/MEDIAL FORM) = Unicode Right-Joining (+ Dual-Joining + Join_Causing) */
2748		$this->arabRightJoining = [
2749			0x0620 => 1, 0x0622 => 1, 0x0623 => 1, 0x0624 => 1, 0x0625 => 1, 0x0626 => 1, 0x0627 => 1, 0x0628 => 1,
2750			0x0629 => 1, 0x062A => 1, 0x062B => 1, 0x062C => 1, 0x062D => 1, 0x062E => 1, 0x062F => 1, 0x0630 => 1,
2751			0x0631 => 1, 0x0632 => 1, 0x0633 => 1, 0x0634 => 1, 0x0635 => 1, 0x0636 => 1, 0x0637 => 1, 0x0638 => 1,
2752			0x0639 => 1, 0x063A => 1, 0x063B => 1, 0x063C => 1, 0x063D => 1, 0x063E => 1, 0x063F => 1, 0x0640 => 1,
2753			0x0641 => 1, 0x0642 => 1, 0x0643 => 1, 0x0644 => 1, 0x0645 => 1, 0x0646 => 1, 0x0647 => 1, 0x0648 => 1,
2754			0x0649 => 1, 0x064A => 1, 0x066E => 1, 0x066F => 1, 0x0671 => 1, 0x0672 => 1, 0x0673 => 1, 0x0675 => 1,
2755			0x0676 => 1, 0x0677 => 1, 0x0678 => 1, 0x0679 => 1, 0x067A => 1, 0x067B => 1, 0x067C => 1, 0x067D => 1,
2756			0x067E => 1, 0x067F => 1, 0x0680 => 1, 0x0681 => 1, 0x0682 => 1, 0x0683 => 1, 0x0684 => 1, 0x0685 => 1,
2757			0x0686 => 1, 0x0687 => 1, 0x0688 => 1, 0x0689 => 1, 0x068A => 1, 0x068B => 1, 0x068C => 1, 0x068D => 1,
2758			0x068E => 1, 0x068F => 1, 0x0690 => 1, 0x0691 => 1, 0x0692 => 1, 0x0693 => 1, 0x0694 => 1, 0x0695 => 1,
2759			0x0696 => 1, 0x0697 => 1, 0x0698 => 1, 0x0699 => 1, 0x069A => 1, 0x069B => 1, 0x069C => 1, 0x069D => 1,
2760			0x069E => 1, 0x069F => 1, 0x06A0 => 1, 0x06A1 => 1, 0x06A2 => 1, 0x06A3 => 1, 0x06A4 => 1, 0x06A5 => 1,
2761			0x06A6 => 1, 0x06A7 => 1, 0x06A8 => 1, 0x06A9 => 1, 0x06AA => 1, 0x06AB => 1, 0x06AC => 1, 0x06AD => 1,
2762			0x06AE => 1, 0x06AF => 1, 0x06B0 => 1, 0x06B1 => 1, 0x06B2 => 1, 0x06B3 => 1, 0x06B4 => 1, 0x06B5 => 1,
2763			0x06B6 => 1, 0x06B7 => 1, 0x06B8 => 1, 0x06B9 => 1, 0x06BA => 1, 0x06BB => 1, 0x06BC => 1, 0x06BD => 1,
2764			0x06BE => 1, 0x06BF => 1, 0x06C0 => 1, 0x06C1 => 1, 0x06C2 => 1, 0x06C3 => 1, 0x06C4 => 1, 0x06C5 => 1,
2765			0x06C6 => 1, 0x06C7 => 1, 0x06C8 => 1, 0x06C9 => 1, 0x06CA => 1, 0x06CB => 1, 0x06CC => 1, 0x06CD => 1,
2766			0x06CE => 1, 0x06CF => 1, 0x06D0 => 1, 0x06D1 => 1, 0x06D2 => 1, 0x06D3 => 1, 0x06D5 => 1, 0x06EE => 1,
2767			0x06EF => 1, 0x06FA => 1, 0x06FB => 1, 0x06FC => 1, 0x06FF => 1,
2768			/* Arabic Supplement */
2769			0x0750 => 1, 0x0751 => 1, 0x0752 => 1, 0x0753 => 1, 0x0754 => 1, 0x0755 => 1, 0x0756 => 1, 0x0757 => 1,
2770			0x0758 => 1, 0x0759 => 1, 0x075A => 1, 0x075B => 1, 0x075C => 1, 0x075D => 1, 0x075E => 1, 0x075F => 1,
2771			0x0760 => 1, 0x0761 => 1, 0x0762 => 1, 0x0763 => 1, 0x0764 => 1, 0x0765 => 1, 0x0766 => 1, 0x0767 => 1,
2772			0x0768 => 1, 0x0769 => 1, 0x076A => 1, 0x076B => 1, 0x076C => 1, 0x076D => 1, 0x076E => 1, 0x076F => 1,
2773			0x0770 => 1, 0x0771 => 1, 0x0772 => 1, 0x0773 => 1, 0x0774 => 1, 0x0775 => 1, 0x0776 => 1, 0x0777 => 1,
2774			0x0778 => 1, 0x0779 => 1, 0x077A => 1, 0x077B => 1, 0x077C => 1, 0x077D => 1, 0x077E => 1, 0x077F => 1,
2775			/* Extended Arabic */
2776			0x08A0 => 1, 0x08A2 => 1, 0x08A3 => 1, 0x08A4 => 1, 0x08A5 => 1, 0x08A6 => 1, 0x08A7 => 1, 0x08A8 => 1,
2777			0x08A9 => 1, 0x08AA => 1, 0x08AB => 1, 0x08AC => 1,
2778			/* 'syrc' Syriac */
2779			0x0710 => 1, 0x0712 => 1, 0x0713 => 1, 0x0714 => 1, 0x0715 => 1, 0x0716 => 1, 0x0717 => 1, 0x0718 => 1,
2780			0x0719 => 1, 0x071A => 1, 0x071B => 1, 0x071C => 1, 0x071D => 1, 0x071E => 1, 0x071F => 1, 0x0720 => 1,
2781			0x0721 => 1, 0x0722 => 1, 0x0723 => 1, 0x0724 => 1, 0x0725 => 1, 0x0726 => 1, 0x0727 => 1, 0x0728 => 1,
2782			0x0729 => 1, 0x072A => 1, 0x072B => 1, 0x072C => 1, 0x072D => 1, 0x072E => 1, 0x072F => 1, 0x074D => 1,
2783			0x074E => 1, 0x074F,
2784			/* N'Ko */
2785			0x07CA => 1, 0x07CB => 1, 0x07CC => 1, 0x07CD => 1, 0x07CE => 1, 0x07CF => 1, 0x07D0 => 1, 0x07D1 => 1,
2786			0x07D2 => 1, 0x07D3 => 1, 0x07D4 => 1, 0x07D5 => 1, 0x07D6 => 1, 0x07D7 => 1, 0x07D8 => 1, 0x07D9 => 1,
2787			0x07DA => 1, 0x07DB => 1, 0x07DC => 1, 0x07DD => 1, 0x07DE => 1, 0x07DF => 1, 0x07E0 => 1, 0x07E1 => 1,
2788			0x07E2 => 1, 0x07E3 => 1, 0x07E4 => 1, 0x07E5 => 1, 0x07E6 => 1, 0x07E7 => 1, 0x07E8 => 1, 0x07E9 => 1,
2789			0x07EA => 1, 0x07FA => 1,
2790			/* Mandaic */
2791			0x0841 => 1, 0x0842 => 1, 0x0843 => 1, 0x0844 => 1, 0x0845 => 1, 0x0847 => 1, 0x0848 => 1, 0x084A => 1,
2792			0x084B => 1, 0x084C => 1, 0x084D => 1, 0x084E => 1, 0x0850 => 1, 0x0851 => 1, 0x0852 => 1, 0x0853 => 1,
2793			0x0855 => 1,
2794			0x0840 => 1, 0x0846 => 1, 0x0849 => 1, 0x084F => 1, 0x0854 => 1, /* Right joining */
2795			/* ZWJ U+200D */
2796			0x0200D => 1];
2797
2798		/* VOWELS = TRANSPARENT-JOINING = Unicode Transparent-Joining type (not just vowels) */
2799		$this->arabTransparent = [
2800			0x0610 => 1, 0x0611 => 1, 0x0612 => 1, 0x0613 => 1, 0x0614 => 1, 0x0615 => 1, 0x0616 => 1, 0x0617 => 1,
2801			0x0618 => 1, 0x0619 => 1, 0x061A => 1, 0x064B => 1, 0x064C => 1, 0x064D => 1, 0x064E => 1, 0x064F => 1,
2802			0x0650 => 1, 0x0651 => 1, 0x0652 => 1, 0x0653 => 1, 0x0654 => 1, 0x0655 => 1, 0x0656 => 1, 0x0657 => 1,
2803			0x0658 => 1, 0x0659 => 1, 0x065A => 1, 0x065B => 1, 0x065C => 1, 0x065D => 1, 0x065E => 1, 0x065F => 1,
2804			0x0670 => 1, 0x06D6 => 1, 0x06D7 => 1, 0x06D8 => 1, 0x06D9 => 1, 0x06DA => 1, 0x06DB => 1, 0x06DC => 1,
2805			0x06DF => 1, 0x06E0 => 1, 0x06E1 => 1, 0x06E2 => 1, 0x06E3 => 1, 0x06E4 => 1, 0x06E7 => 1, 0x06E8 => 1,
2806			0x06EA => 1, 0x06EB => 1, 0x06EC => 1, 0x06ED => 1,
2807			/* Extended Arabic */
2808			0x08E4 => 1, 0x08E5 => 1, 0x08E6 => 1, 0x08E7 => 1, 0x08E8 => 1, 0x08E9 => 1, 0x08EA => 1, 0x08EB => 1,
2809			0x08EC => 1, 0x08ED => 1, 0x08EE => 1, 0x08EF => 1, 0x08F0 => 1, 0x08F1 => 1, 0x08F2 => 1, 0x08F3 => 1,
2810			0x08F4 => 1, 0x08F5 => 1, 0x08F6 => 1, 0x08F7 => 1, 0x08F8 => 1, 0x08F9 => 1, 0x08FA => 1, 0x08FB => 1,
2811			0x08FC => 1, 0x08FD => 1, 0x08FE => 1,
2812			/* Arabic ligatures in presentation form (converted in 'ccmp' in e.g. Arial and Times ? need to add others in this range) */
2813			0xFC5E => 1, 0xFC5F => 1, 0xFC60 => 1, 0xFC61 => 1, 0xFC62 => 1,
2814			/*  'syrc' Syriac */
2815			0x070F => 1, 0x0711 => 1, 0x0730 => 1, 0x0731 => 1, 0x0732 => 1, 0x0733 => 1, 0x0734 => 1, 0x0735 => 1,
2816			0x0736 => 1, 0x0737 => 1, 0x0738 => 1, 0x0739 => 1, 0x073A => 1, 0x073B => 1, 0x073C => 1, 0x073D => 1,
2817			0x073E => 1, 0x073F => 1, 0x0740 => 1, 0x0741 => 1, 0x0742 => 1, 0x0743 => 1, 0x0744 => 1, 0x0745 => 1,
2818			0x0746 => 1, 0x0747 => 1, 0x0748 => 1, 0x0749 => 1, 0x074A => 1,
2819			/* N'Ko */
2820			0x07EB => 1, 0x07EC => 1, 0x07ED => 1, 0x07EE => 1, 0x07EF => 1, 0x07F0 => 1, 0x07F1 => 1, 0x07F2 => 1,
2821			0x07F3 => 1,
2822			/* Mandaic */
2823			0x0859 => 1, 0x085A => 1, 0x085B => 1,
2824		];
2825	}
2826
2827	private function arabic_shaper($usetags, $scriptTag)
2828	{
2829		$chars = [];
2830		for ($i = 0; $i < count($this->OTLdata); $i++) {
2831			$chars[] = $this->OTLdata[$i]['hex'];
2832		}
2833
2834		$crntChar = null;
2835		$prevChar = null;
2836		$nextChar = null;
2837		$output = [];
2838		$max = count($chars);
2839		for ($i = $max - 1; $i >= 0; $i--) {
2840			$crntChar = $chars[$i];
2841			if ($i > 0) {
2842				$prevChar = hexdec($chars[$i - 1]);
2843			} else {
2844				$prevChar = null;
2845			}
2846			if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 2])) {
2847				$prevChar = hexdec($chars[$i - 2]);
2848				if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 3])) {
2849					$prevChar = hexdec($chars[$i - 3]);
2850					if ($prevChar && isset($this->arabTransparentJoin[$prevChar]) && isset($chars[$i - 4])) {
2851						$prevChar = hexdec($chars[$i - 4]);
2852					}
2853				}
2854			}
2855			if ($crntChar && isset($this->arabTransparentJoin[hexdec($crntChar)])) {
2856				// If next_char = RightJoining && prev_char = LeftJoining:
2857				if (isset($chars[$i + 1]) && $chars[$i + 1] && isset($this->arabRightJoining[hexdec($chars[$i + 1])]) && $prevChar && isset($this->arabLeftJoining[$prevChar])) {
2858					$output[] = $this->get_arab_glyphs($crntChar, 1, $chars, $i, $scriptTag, $usetags); // <final> form
2859				} else {
2860					$output[] = $this->get_arab_glyphs($crntChar, 0, $chars, $i, $scriptTag, $usetags);  // <isolated> form
2861				}
2862				continue;
2863			}
2864			if (hexdec($crntChar) < 128) {
2865				$output[] = [$crntChar, 0];
2866				$nextChar = $crntChar;
2867				continue;
2868			}
2869			// 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL
2870			$form = 0;
2871			if ($prevChar && isset($this->arabLeftJoining[$prevChar])) {
2872				$form++;
2873			}
2874			if ($nextChar && isset($this->arabRightJoining[hexdec($nextChar)])) {
2875				$form += 2;
2876			}
2877			$output[] = $this->get_arab_glyphs($crntChar, $form, $chars, $i, $scriptTag, $usetags);
2878			$nextChar = $crntChar;
2879		}
2880		$ra = array_reverse($output);
2881		for ($i = 0; $i < count($this->OTLdata); $i++) {
2882			$this->OTLdata[$i]['uni'] = hexdec($ra[$i][0]);
2883			$this->OTLdata[$i]['hex'] = $ra[$i][0];
2884			$this->OTLdata[$i]['form'] = $ra[$i][1]; // Actaul form substituted 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL
2885		}
2886	}
2887
2888	private function get_arab_glyphs($char, $type, &$chars, $i, $scriptTag, $usetags)
2889	{
2890		// Optional Feature settings    // doesn't control Syriac at present
2891		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)) {
2892			return [$char, 0];
2893		}
2894
2895		// 0=ISOLATED FORM :: 1=FINAL :: 2=INITIAL :: 3=MEDIAL (:: 4=MED2 :: 5=FIN2 :: 6=FIN3)
2896		$retk = -1;
2897		// Alaph 00710 in Syriac
2898		if ($scriptTag == 'syrc' && $char == '00710') {
2899			// if there is a preceding (base?) character *** should search back to previous base - ignoring vowels and change $n
2900			// set $n as the position of the last base; for now we'll just do this:
2901			$n = $i - 1;
2902			// if the preceding (base) character cannot be joined to
2903			// not in $this->arabLeftJoining i.e. not a char which can join to the next one
2904			if (isset($chars[$n]) && isset($this->arabLeftJoining[hexdec($chars[$n])])) {
2905				// if in the middle of Syriac words
2906				if (isset($chars[$i + 1]) && preg_match('/[\x{0700}-\x{0745}]/u', code2utf(hexdec($chars[$n]))) && preg_match('/[\x{0700}-\x{0745}]/u', code2utf(hexdec($chars[$i + 1]))) && isset($this->arabGlyphs[$char][4])) {
2907					$retk = 4;
2908				} // if at the end of Syriac words
2909				elseif (!isset($chars[$i + 1]) || !preg_match('/[\x{0700}-\x{0745}]/u', code2utf(hexdec($chars[$i + 1])))) {
2910					// if preceding base character IS (00715|00716|0072A)
2911					if (strpos('0715|0716|072A', $chars[$n]) !== false && isset($this->arabGlyphs[$char][6])) {
2912						$retk = 6;
2913					} // elseif preceding base character is NOT (00715|00716|0072A)
2914					elseif (isset($this->arabGlyphs[$char][5])) {
2915						$retk = 5;
2916					}
2917				}
2918			}
2919			if ($retk != -1) {
2920				return [$this->arabGlyphs[$char][$retk], $retk];
2921			} else {
2922				return [$char, 0];
2923			}
2924		}
2925
2926		if (($type > 0 || $type === 0) && isset($this->arabGlyphs[$char][$type])) {
2927			$retk = $type;
2928		} elseif ($type == 3 && isset($this->arabGlyphs[$char][1])) { // if <medial> not defined, but <final>, return <final>
2929			$retk = 1;
2930		} elseif ($type == 2 && isset($this->arabGlyphs[$char][0])) { // if <initial> not defined, but <isolated>, return <isolated>
2931			$retk = 0;
2932		}
2933		if ($retk != -1) {
2934			$match = true;
2935			// If GSUB includes a Backtrack or Lookahead condition (e.g. font ArabicTypesetting)
2936			if (isset($this->arabGlyphs[$char]['prel'][$retk]) && $this->arabGlyphs[$char]['prel'][$retk]) {
2937				$ig = 1;
2938				foreach ($this->arabGlyphs[$char]['prel'][$retk] as $k => $v) { // $k starts 0, 1...
2939					if (!isset($chars[$i - $ig - $k])) {
2940						$match = false;
2941					} elseif (strpos($v, $chars[$i - $ig - $k]) === false) {
2942						while (strpos($this->arabGlyphs[$char]['ignore'][$retk], $chars[$i - $ig - $k]) !== false) {  // ignore
2943							$ig++;
2944						}
2945						if (!isset($chars[$i - $ig - $k])) {
2946							$match = false;
2947						} elseif (strpos($v, $chars[$i - $ig - $k]) === false) {
2948							$match = false;
2949						}
2950					}
2951				}
2952			}
2953			if (isset($this->arabGlyphs[$char]['postl'][$retk]) && $this->arabGlyphs[$char]['postl'][$retk]) {
2954				$ig = 1;
2955				foreach ($this->arabGlyphs[$char]['postl'][$retk] as $k => $v) { // $k starts 0, 1...
2956					if (!isset($chars[$i + $ig + $k])) {
2957						$match = false;
2958					} elseif (strpos($v, $chars[$i + $ig + $k]) === false) {
2959						while (strpos($this->arabGlyphs[$char]['ignore'][$retk], $chars[$i + $ig + $k]) !== false) {  // ignore
2960							$ig++;
2961						}
2962						if (!isset($chars[$i + $ig + $k])) {
2963							$match = false;
2964						} elseif (strpos($v, $chars[$i + $ig + $k]) === false) {
2965							$match = false;
2966						}
2967					}
2968				}
2969			}
2970			if ($match) {
2971				return [$this->arabGlyphs[$char][$retk], $retk];
2972			} else {
2973				return [$char, 0];
2974			}
2975		} else {
2976			return [$char, 0];
2977		}
2978	}
2979
2980	////////////////////////////////////////////////////////////////
2981	/////////////////       LINE BREAKING    ///////////////////////
2982	////////////////////////////////////////////////////////////////
2983	/////////////       TIBETAN LINE BREAKING    ///////////////////
2984	////////////////////////////////////////////////////////////////
2985	// Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
2986	private function tibetanLineBreaking()
2987	{
2988		for ($ptr = 0; $ptr < count($this->OTLdata); $ptr++) {
2989			// Break opportunities at U+0F0B Tsheg or U=0F0D
2990			if (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] == 0x0F0B || $this->OTLdata[$ptr]['uni'] == 0x0F0D)) {
2991				if (isset($this->OTLdata[$ptr + 1]['uni']) && ($this->OTLdata[$ptr + 1]['uni'] == 0x0F0D || $this->OTLdata[$ptr + 1]['uni'] == 0xF0E)) {
2992					continue;
2993				}
2994				// Set end of word marker in OTLdata at matchpos
2995				$this->OTLdata[$ptr]['wordend'] = true;
2996			}
2997		}
2998	}
2999
3000	/**
3001	 * South East Asian Linebreaking (Thai, Khmer and Lao) using dictionary of words
3002	 *
3003	 * Sets $this->OTLdata[$i]['wordend']=true at possible end of word boundaries
3004	 */
3005	private function seaLineBreaking()
3006	{
3007		// Load Line-breaking dictionary
3008		if (!isset($this->lbdicts[$this->shaper]) && file_exists(__DIR__ . '/../data/linebrdict' . $this->shaper . '.dat')) {
3009			$this->lbdicts[$this->shaper] = file_get_contents(__DIR__ . '/../data/linebrdict' . $this->shaper . '.dat');
3010		}
3011
3012		$dict = &$this->lbdicts[$this->shaper];
3013
3014		// Find all word boundaries and mark end of word $this->OTLdata[$i]['wordend']=true on last character
3015		// If Thai, allow for possible suffixes (not in Lao or Khmer)
3016		// repeater/ellision characters
3017		// (0x0E2F);        // Ellision character THAI_PAIYANNOI 0x0E2F  UTF-8 0xE0 0xB8 0xAF
3018		// (0x0E46);        // Repeat character THAI_MAIYAMOK 0x0E46   UTF-8 0xE0 0xB9 0x86
3019		// (0x0EC6);        // Repeat character LAO   UTF-8 0xE0 0xBB 0x86
3020
3021		$rollover = [];
3022		$ptr = 0;
3023
3024		while ($ptr < count($this->OTLdata) - 3) {
3025			if (count($rollover)) {
3026				$matches = $rollover;
3027				$rollover = [];
3028			} else {
3029				$matches = $this->checkwordmatch($dict, $ptr);
3030			}
3031			if (count($matches) == 1) {
3032				$matchpos = $matches[0];
3033				// Check for repeaters - if so $matchpos++
3034				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)) {
3035					$matchpos++;
3036				}
3037				// Set end of word marker in OTLdata at matchpos
3038				$this->OTLdata[$matchpos]['wordend'] = true;
3039				$ptr = $matchpos + 1;
3040			} elseif (empty($matches)) {
3041				$ptr++;
3042				// Move past any ASCII characters
3043				while (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] >> 8) == 0) {
3044					$ptr++;
3045				}
3046			} else { // Multiple matches
3047				$secondmatch = false;
3048				for ($m = count($matches) - 1; $m >= 0; $m--) {
3049					//for ($m=0;$m<count($matches);$m++) {
3050					$firstmatch = $matches[$m];
3051					$matches2 = $this->checkwordmatch($dict, $firstmatch + 1);
3052					if (count($matches2)) {
3053						// Set end of word marker in OTLdata at matchpos
3054						$this->OTLdata[$firstmatch]['wordend'] = true;
3055						$ptr = $firstmatch + 1;
3056						$rollover = $matches2;
3057						$secondmatch = true;
3058						break;
3059					}
3060				}
3061				if (!$secondmatch) {
3062					// Set end of word marker in OTLdata at end of longest first match
3063					$this->OTLdata[$matches[count($matches) - 1]]['wordend'] = true;
3064					$ptr = $matches[count($matches) - 1] + 1;
3065					// Move past any ASCII characters
3066					while (isset($this->OTLdata[$ptr]['uni']) && ($this->OTLdata[$ptr]['uni'] >> 8) == 0) {
3067						$ptr++;
3068					}
3069				}
3070			}
3071		}
3072	}
3073
3074	private function checkwordmatch(&$dict, $ptr)
3075	{
3076		/*
3077		  Node type: Split.
3078		  Divide at < 98 >= 98
3079		  Offset for >= 98 == 79    (long 4-byte unsigned)
3080
3081		  Node type: Linear match.
3082		  Char = 97
3083
3084		  Intermediate match
3085
3086		  Final match
3087		 */
3088
3089		$dictptr = 0;
3090		$ok = true;
3091		$matches = [];
3092		while ($ok) {
3093			$x = ord($dict{$dictptr});
3094			$c = $this->OTLdata[$ptr]['uni'] & 0xFF;
3095			if ($x == static::_DICT_INTERMEDIATE_MATCH) {
3096//echo "DICT_INTERMEDIATE_MATCH: ".dechex($c).'<br />';
3097				// Do not match if next character in text is a Mark
3098				if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3099					$matches[] = $ptr - 1;
3100				}
3101				$dictptr++;
3102			} elseif ($x == static::_DICT_FINAL_MATCH) {
3103//echo "DICT_FINAL_MATCH: ".dechex($c).'<br />';
3104				// Do not match if next character in text is a Mark
3105				if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3106					$matches[] = $ptr - 1;
3107				}
3108				return $matches;
3109			} elseif ($x == static::_DICT_NODE_TYPE_LINEAR) {
3110//echo "DICT_NODE_TYPE_LINEAR: ".dechex($c).'<br />';
3111				$dictptr++;
3112				$m = ord($dict{$dictptr});
3113				if ($c == $m) {
3114					$ptr++;
3115					if ($ptr > count($this->OTLdata) - 1) {
3116						$next = ord($dict{$dictptr + 1});
3117						if ($next == static::_DICT_INTERMEDIATE_MATCH || $next == static::_DICT_FINAL_MATCH) {
3118							// Do not match if next character in text is a Mark
3119							if (isset($this->OTLdata[$ptr]['uni']) && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3120								$matches[] = $ptr - 1;
3121							}
3122						}
3123						return $matches;
3124					}
3125					$dictptr++;
3126					continue;
3127				} else {
3128//echo "DICT_NODE_TYPE_LINEAR NOT: ".dechex($c).'<br />';
3129					return $matches;
3130				}
3131			} elseif ($x == static::_DICT_NODE_TYPE_SPLIT) {
3132//echo "DICT_NODE_TYPE_SPLIT ON ".dechex($d).": ".dechex($c).'<br />';
3133				$dictptr++;
3134				$d = ord($dict{$dictptr});
3135				if ($c < $d) {
3136					$dictptr += 5;
3137				} else {
3138					$dictptr++;
3139					// Unsigned long 32-bit offset
3140					$offset = (ord($dict{$dictptr}) * 16777216) + (ord($dict{$dictptr + 1}) << 16) + (ord($dict{$dictptr + 2}) << 8) + ord($dict{$dictptr + 3});
3141					$dictptr = $offset;
3142				}
3143			} else {
3144//echo "PROBLEM: ".($x).'<br />';
3145				$ok = false; // Something has gone wrong
3146			}
3147		}
3148
3149		return $matches;
3150	}
3151
3152	////////////////////////////////////////////////////////////////
3153	//////////       GPOS    ///////////////////////////////////////
3154	////////////////////////////////////////////////////////////////
3155	private function _applyGPOSrules($LookupList, $is_old_spec = false)
3156	{
3157		foreach ($LookupList as $lu => $tag) {
3158			$Type = $this->GPOSLookups[$lu]['Type'];
3159			$Flag = $this->GPOSLookups[$lu]['Flag'];
3160			$MarkFilteringSet = '';
3161			if (isset($this->GPOSLookups[$lu]['MarkFilteringSet'])) {
3162				$MarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
3163			}
3164			$ptr = 0;
3165			// Test each glyph sequentially
3166			while ($ptr < (count($this->OTLdata))) { // whilst there is another glyph ..0064
3167				$currGlyph = $this->OTLdata[$ptr]['hex'];
3168				$currGID = $this->OTLdata[$ptr]['uni'];
3169				$shift = 1;
3170				foreach ($this->GPOSLookups[$lu]['Subtables'] as $c => $subtable_offset) {
3171					// NB Coverage only looks at glyphs for position 1 (esp. 7.3 and 8.3)
3172					if (isset($this->LuCoverage[$lu][$c][$currGID])) {
3173						// Get rules from font GPOS subtable
3174						if (isset($this->OTLdata[$ptr]['bidi_type'])) {  // No need to check bidi_type - just a check that it exists
3175							$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);
3176							if ($shift) {
3177								break;
3178							}
3179						}
3180					}
3181				}
3182				if ($shift == 0) {
3183					$shift = 1;
3184				}
3185				$ptr += $shift;
3186			}
3187		}
3188	}
3189
3190	//////////////////////////////////////////////////////////////////////////////////
3191	// GPOS Types
3192	// Lookup Type 1: Single Adjustment Positioning Subtable        Adjust position of a single glyph
3193	// Lookup Type 2: Pair Adjustment Positioning Subtable      Adjust position of a pair of glyphs
3194	// Lookup Type 3: Cursive Attachment Positioning Subtable       Attach cursive glyphs
3195	// Lookup Type 4: MarkToBase Attachment Positioning Subtable    Attach a combining mark to a base glyph
3196	// Lookup Type 5: MarkToLigature Attachment Positioning Subtable    Attach a combining mark to a ligature
3197	// Lookup Type 6: MarkToMark Attachment Positioning Subtable    Attach a combining mark to another mark
3198	// Lookup Type 7: Contextual Positioning Subtables          Position one or more glyphs in context
3199	// Lookup Type 8: Chaining Contextual Positioning Subtable      Position one or more glyphs in chained context
3200	// Lookup Type 9: Extension positioning
3201	//////////////////////////////////////////////////////////////////////////////////
3202	private function _applyGPOSvaluerecord($basepos, $Value)
3203	{
3204
3205		// If current glyph is a mark with a defined width, any XAdvance is considered to REPLACE the character Advance Width
3206		// Test case <div style="font-family:myanmartext">&#x1004;&#x103a;&#x1039;&#x1000;&#x1039;&#x1000;&#x103b;&#x103c;&#x103d;&#x1031;&#x102d;</div>
3207		if (strpos($this->GlyphClassMarks, $this->OTLdata[$basepos]['hex']) !== false) {
3208			$cw = round($this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$basepos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000); // convert back to font design units
3209		} else {
3210			$cw = 0;
3211		}
3212
3213		$apos = $this->_getXAdvancePos($basepos);
3214
3215		if (isset($Value['XAdvance']) && ($Value['XAdvance'] - $cw) != 0) {
3216			// However DON'T REPLACE the character Advance Width if Advance Width is negative
3217			// Test case <div style="font-family: dejavusansmono">&#x440;&#x443;&#x301;&#x441;&#x441;&#x43a;&#x438;&#x439;</div>
3218			if ($Value['XAdvance'] < 0) {
3219				$cw = 0;
3220			}
3221
3222			// For LTR apply XAdvanceL to the last mark following the base = at $apos
3223			// For RTL apply XAdvanceR to base = at $basepos
3224			if (isset($this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'])) {
3225				$this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'] += $Value['XAdvance'] - $cw;
3226			} else {
3227				$this->OTLdata[$apos]['GPOSinfo']['XAdvanceL'] = $Value['XAdvance'] - $cw;
3228			}
3229			if (isset($this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'])) {
3230				$this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'] += $Value['XAdvance'] - $cw;
3231			} else {
3232				$this->OTLdata[$basepos]['GPOSinfo']['XAdvanceR'] = $Value['XAdvance'] - $cw;
3233			}
3234		}
3235
3236		// Any XPlacement (? and Y Placement) apply to base and marks (from basepos to apos)
3237		for ($a = $basepos; $a <= $apos; $a++) {
3238			if (isset($Value['XPlacement'])) {
3239				if (isset($this->OTLdata[$a]['GPOSinfo']['XPlacement'])) {
3240					$this->OTLdata[$a]['GPOSinfo']['XPlacement'] += $Value['XPlacement'];
3241				} else {
3242					$this->OTLdata[$a]['GPOSinfo']['XPlacement'] = $Value['XPlacement'];
3243				}
3244			}
3245			if (isset($Value['YPlacement'])) {
3246				if (isset($this->OTLdata[$a]['GPOSinfo']['YPlacement'])) {
3247					$this->OTLdata[$a]['GPOSinfo']['YPlacement'] += $Value['YPlacement'];
3248				} else {
3249					$this->OTLdata[$a]['GPOSinfo']['YPlacement'] = $Value['YPlacement'];
3250				}
3251			}
3252		}
3253	}
3254
3255	// If XAdvance is aplied to $ptr - in order for PDF to position the Advance correctly need to place it on
3256	// the last of any Marks which immediately follow the current glyph
3257	private function _getXAdvancePos($pos)
3258	{
3259		// NB Not all fonts have all marks specified in GlyphClassMarks
3260		// If the current glyph is not a base (but a mark) then ignore this, and apply to the current position
3261		if (strpos($this->GlyphClassMarks, $this->OTLdata[$pos]['hex']) !== false) {
3262			return $pos;
3263		}
3264
3265		while (isset($this->OTLdata[$pos + 1]['hex']) && strpos($this->GlyphClassMarks, $this->OTLdata[$pos + 1]['hex']) !== false) {
3266			$pos++;
3267		}
3268		return $pos;
3269	}
3270
3271	private function _applyGPOSsubtable($lookupID, $subtable, $ptr, $currGlyph, $currGID, $subtable_offset, $Type, $Flag, $MarkFilteringSet, $LuCoverage, $tag, $level, $is_old_spec)
3272	{
3273		if (($Flag & 0x0001) == 1) {
3274			$dir = 'RTL';
3275		} else { // only used for Type 3
3276			$dir = 'LTR';
3277		}
3278
3279		$ignore = $this->_getGCOMignoreString($Flag, $MarkFilteringSet);
3280
3281		// Lets start
3282		$this->seek($subtable_offset);
3283		$PosFormat = $this->read_ushort();
3284
3285		////////////////////////////////////////////////////////////////////////////////
3286		// LookupType 1: Single adjustment  Adjust position of a single glyph (e.g. SmallCaps/Sups/Subs)
3287		////////////////////////////////////////////////////////////////////////////////
3288		if ($Type == 1) {
3289			//===========
3290			// Format 1:
3291			//===========
3292			if ($PosFormat == 1) {
3293				$Coverage = $subtable_offset + $this->read_ushort();
3294				$ValueFormat = $this->read_ushort();
3295				$Value = $this->_getValueRecord($ValueFormat);
3296			} //===========
3297			// Format 2:
3298			//===========
3299			elseif ($PosFormat == 2) {
3300				$Coverage = $subtable_offset + $this->read_ushort();
3301				$ValueFormat = $this->read_ushort();
3302				$ValueCount = $this->read_ushort();
3303				$GlyphPos = $LuCoverage[$currGID];
3304				$this->skip($GlyphPos * 2 * $this->count_bits($ValueFormat));
3305				$Value = $this->_getValueRecord($ValueFormat);
3306			}
3307			$this->_applyGPOSvaluerecord($ptr, $Value);
3308			if ($this->debugOTL) {
3309				$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3310			}
3311			return 1;
3312		} ////////////////////////////////////////////////////////////////////////////////
3313		// LookupType 2: Pair adjustment    Adjust position of a pair of glyphs (Kerning)
3314		////////////////////////////////////////////////////////////////////////////////
3315		elseif ($Type == 2) {
3316			$Coverage = $subtable_offset + $this->read_ushort();
3317			$ValueFormat1 = $this->read_ushort();
3318			$ValueFormat2 = $this->read_ushort();
3319			$sizeOfPair = ( 2 * $this->count_bits($ValueFormat1) ) + ( 2 * $this->count_bits($ValueFormat2) );
3320			//===========
3321			// Format 1:
3322			//===========
3323			if ($PosFormat == 1) {
3324				$PairSetCount = $this->read_ushort();
3325				$PairSetOffset = [];
3326				for ($p = 0; $p < $PairSetCount; $p++) {
3327					$PairSetOffset[] = $subtable_offset + $this->read_ushort();
3328				}
3329				for ($p = 0; $p < $PairSetCount; $p++) {
3330					if (isset($LuCoverage[$currGID]) && $LuCoverage[$currGID] == $p) {
3331						$this->seek($PairSetOffset[$p]);
3332						//PairSet table
3333						$PairValueCount = $this->read_ushort();
3334						for ($pv = 0; $pv < $PairValueCount; $pv++) {
3335							//PairValueRecord
3336							$gid = $this->read_ushort();
3337							$SecondGlyph = $this->glyphToChar($gid);
3338							$FirstGlyph = $this->OTLdata[$ptr]['uni'];
3339
3340							$checkpos = $ptr;
3341							$checkpos++;
3342							while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
3343								$checkpos++;
3344							}
3345							if (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == $SecondGlyph) {
3346								$matchedpos = $checkpos;
3347							} else {
3348								$matchedpos = false;
3349							}
3350
3351							if ($matchedpos !== false) {
3352								$Value1 = $this->_getValueRecord($ValueFormat1);
3353								$Value2 = $this->_getValueRecord($ValueFormat2);
3354								if ($ValueFormat1) {
3355									$this->_applyGPOSvaluerecord($ptr, $Value1);
3356								}
3357								if ($ValueFormat2) {
3358									$this->_applyGPOSvaluerecord($matchedpos, $Value2);
3359									if ($this->debugOTL) {
3360										$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3361									}
3362									return $matchedpos - $ptr + 1;
3363								}
3364								if ($this->debugOTL) {
3365									$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3366								}
3367								return $matchedpos - $ptr;
3368							} else {
3369								$this->skip($sizeOfPair);
3370							}
3371						}
3372					}
3373				}
3374				return 0;
3375			} //===========
3376			// Format 2:
3377			//===========
3378			elseif ($PosFormat == 2) {
3379				$ClassDef1 = $subtable_offset + $this->read_ushort();
3380				$ClassDef2 = $subtable_offset + $this->read_ushort();
3381				$Class1Count = $this->read_ushort();
3382				$Class2Count = $this->read_ushort();
3383
3384				$sizeOfValueRecords = $Class1Count * $Class2Count * $sizeOfPair;
3385
3386				//$this->skip($sizeOfValueRecords );  ???? NOT NEEDED
3387				// NB Class1Count includes Class 0 even though it is not defined by $ClassDef1
3388				// i.e. Class1Count = 5; Class1 will contain array(indices 1-4);
3389				$Class1 = $this->_getClassDefinitionTable($ClassDef1);
3390				$Class2 = $this->_getClassDefinitionTable($ClassDef2);
3391				$FirstGlyph = $this->OTLdata[$ptr]['uni'];
3392				$checkpos = $ptr;
3393				$checkpos++;
3394				while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
3395					$checkpos++;
3396				}
3397				if (isset($this->OTLdata[$checkpos])) {
3398					$matchedpos = $checkpos;
3399				} else {
3400					return 0;
3401				}
3402
3403				$SecondGlyph = $this->OTLdata[$matchedpos]['uni'];
3404				for ($i = 0; $i < $Class1Count; $i++) {
3405					if (isset($Class1[$i]) && count($Class1[$i])) {
3406						$FirstClassPos = array_search($FirstGlyph, $Class1[$i]);
3407						if ($FirstClassPos === false) {
3408							continue;
3409						} else {
3410							for ($j = 0; $j < $Class2Count; $j++) {
3411								if (isset($Class2[$j]) && count($Class2[$j])) {
3412									$SecondClassPos = array_search($SecondGlyph, $Class2[$j]);
3413									if ($SecondClassPos === false) {
3414										continue;
3415									}
3416
3417									// Get ValueRecord[$i][$j]
3418									$offs = ($i * $Class2Count * $sizeOfPair) + ($j * $sizeOfPair);
3419									$this->seek($subtable_offset + 16 + $offs);
3420
3421									$Value1 = $this->_getValueRecord($ValueFormat1);
3422									$Value2 = $this->_getValueRecord($ValueFormat2);
3423									if ($ValueFormat1) {
3424										$this->_applyGPOSvaluerecord($ptr, $Value1);
3425									}
3426									if ($ValueFormat2) {
3427										$this->_applyGPOSvaluerecord($matchedpos, $Value2);
3428										if ($this->debugOTL) {
3429											$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3430										}
3431										return $matchedpos - $ptr + 1;
3432									}
3433									if ($this->debugOTL) {
3434										$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3435									}
3436									return $matchedpos - $ptr;
3437								}
3438							}
3439						}
3440					}
3441				}
3442				return 0;
3443			}
3444		} ////////////////////////////////////////////////////////////////////////////////
3445		// LookupType 3: Cursive attachment     Attach cursive glyphs
3446		////////////////////////////////////////////////////////////////////////////////
3447		elseif ($Type == 3) {
3448			$this->skip(4);
3449			// Need default XAdvance for glyph
3450			$pdfWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], hexdec($currGlyph)); // DON'T convert back to design units
3451
3452			$CPos = $LuCoverage[$currGID];
3453			$this->skip($CPos * 4);
3454			$EntryAnchor = $this->read_ushort();
3455			$ExitAnchor = $this->read_ushort();
3456			if ($EntryAnchor != 0) {
3457				$EntryAnchor += $subtable_offset;
3458				list($x, $y) = $this->_getAnchorTable($EntryAnchor);
3459				if ($dir == 'RTL') {
3460					if (round($pdfWidth) == round($x * 1000 / $this->mpdf->CurrentFont['unitsPerEm'])) {
3461						$x = 0;
3462					} else {
3463						$x = $x - ($pdfWidth * $this->mpdf->CurrentFont['unitsPerEm'] / 1000);
3464					}
3465				}
3466
3467				$this->Entry[$ptr] = ['X' => $x, 'Y' => $y, 'dir' => $dir];
3468			}
3469			if ($ExitAnchor != 0) {
3470				$ExitAnchor += $subtable_offset;
3471				list($x, $y) = $this->_getAnchorTable($ExitAnchor);
3472				if ($dir == 'LTR') {
3473					if (round($pdfWidth) == round($x * 1000 / $this->mpdf->CurrentFont['unitsPerEm'])) {
3474						$x = 0;
3475					} else {
3476						$x = $x - ($pdfWidth * $this->mpdf->CurrentFont['unitsPerEm'] / 1000);
3477					}
3478				}
3479				$this->Exit[$ptr] = ['X' => $x, 'Y' => $y, 'dir' => $dir];
3480			}
3481			if ($this->debugOTL) {
3482				$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3483			}
3484			return 1;
3485		} ////////////////////////////////////////////////////////////////////////////////
3486		// LookupType 4: MarkToBase attachment  Attach a combining mark to a base glyph
3487		////////////////////////////////////////////////////////////////////////////////
3488		elseif ($Type == 4) {
3489			$MarkCoverage = $subtable_offset + $this->read_ushort();
3490			//$MarkCoverage is already set in $LuCoverage 00065|00073 etc
3491			$BaseCoverage = $subtable_offset + $this->read_ushort();
3492			$ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3493			$MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3494			$BaseArray = $subtable_offset + $this->read_ushort(); // Offset to BaseArray table
3495
3496			$this->seek($BaseCoverage);
3497			$BaseGlyphs = implode('|', $this->_getCoverage());
3498
3499			$checkpos = $ptr;
3500			$checkpos--;
3501
3502			// ZZZ93
3503			// In Lohit-Kannada font (old-spec), rules specify a Type 4 GPOS to attach below-forms to base glyph
3504			// the repositioning does not happen in MS Word, and shouldn't happen comparing with other fonts
3505			// ?Why not
3506			// This Fix blocks the GPOS rule if the "mark" is not actually classified as a mark in the GlyphClasses of GDEF
3507			// but only in Indic old-spec.
3508			// Test cases: &#xca8;&#xccd;&#xca8;&#xcc1; and &#xc95;&#xccd;&#xcb0;&#xccc;
3509			if ($this->shaper == 'I' && $is_old_spec && strpos($this->GlyphClassMarks, $this->OTLdata[$ptr]['hex']) === false) {
3510				return;
3511			}
3512
3513
3514			// "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."
3515			while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
3516				$checkpos--;
3517			}
3518
3519			if (isset($this->OTLdata[$checkpos]) && strpos($BaseGlyphs, $this->OTLdata[$checkpos]['hex']) !== false) {
3520				$matchedpos = $checkpos;
3521			} else {
3522				$matchedpos = false;
3523			}
3524
3525			if ($matchedpos !== false) {
3526				// Get the relevant MarkRecord
3527				$MarkPos = $LuCoverage[$currGID];
3528				$MarkRecord = $this->_getMarkRecord($MarkArray, $MarkPos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3529				//Mark Class is = $MarkRecord['Class']
3530				// Get the relevant BaseRecord
3531				$this->seek($BaseArray);
3532				$BaseCount = $this->read_ushort();
3533				$BasePos = strpos($BaseGlyphs, $this->OTLdata[$matchedpos]['hex']) / 6;
3534
3535				// Move to the BaseRecord we want
3536				$nSkip = (2 * $BasePos * $ClassCount );
3537				$this->skip($nSkip);
3538
3539				// Read BaseRecord we want for appropriate Class
3540				$nSkip = 2 * $MarkRecord['Class'];
3541				$this->skip($nSkip);
3542				$BaseRecordOffset = $BaseArray + $this->read_ushort();
3543				list($x, $y) = $this->_getAnchorTable($BaseRecordOffset);
3544				$BaseRecord = ['AnchorX' => $x, 'AnchorY' => $y]; // e.g. Array ( [AnchorX] => 660 [AnchorY] => 1556 )
3545				// Need default XAdvance for Base glyph
3546				$BaseWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3547				$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $BaseWidth;
3548				// And any intervening (ignored) characters
3549				if (($ptr - $matchedpos) > 1) {
3550					for ($i = $matchedpos + 1; $i < $ptr; $i++) {
3551						$BaseWidthExtra = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$i]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3552						$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] += $BaseWidthExtra;
3553					}
3554				}
3555
3556				// Align to previous Glyph by attachment - so need to add to previous placement values
3557				$prevXPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'] : 0);
3558				$prevYPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'] : 0);
3559
3560				$this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $BaseRecord['AnchorX'] - $MarkRecord['AnchorX'];
3561				$this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $BaseRecord['AnchorY'] - $MarkRecord['AnchorY'];
3562				if ($this->debugOTL) {
3563					$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3564				}
3565				return 1;
3566			}
3567			return 0;
3568		} ////////////////////////////////////////////////////////////////////////////////
3569		// LookupType 5: MarkToLigature attachment  Attach a combining mark to a ligature
3570		////////////////////////////////////////////////////////////////////////////////
3571		elseif ($Type == 5) {
3572			$MarkCoverage = $subtable_offset + $this->read_ushort();
3573			//$MarkCoverage is already set in $LuCoverage 00065|00073 etc
3574			$LigatureCoverage = $subtable_offset + $this->read_ushort();
3575			$ClassCount = $this->read_ushort(); // Number of classes defined for marks = Number of mark glyphs in the MarkCoverage table
3576			$MarkArray = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3577			$LigatureArray = $subtable_offset + $this->read_ushort(); // Offset to LigatureArray table
3578
3579			$this->seek($LigatureCoverage);
3580			$LigatureGlyphs = implode('|', $this->_getCoverage());
3581
3582
3583			$checkpos = $ptr;
3584			$checkpos--;
3585
3586			// "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."
3587			while (isset($this->OTLdata[$checkpos]) && strpos($this->GlyphClassMarks, $this->OTLdata[$checkpos]['hex']) !== false) {
3588				$checkpos--;
3589			}
3590
3591			if (isset($this->OTLdata[$checkpos]) && strpos($LigatureGlyphs, $this->OTLdata[$checkpos]['hex']) !== false) {
3592				$matchedpos = $checkpos;
3593			} else {
3594				$matchedpos = false;
3595			}
3596
3597			if ($matchedpos !== false) {
3598				// Get the relevant MarkRecord
3599				$MarkPos = $LuCoverage[$currGID];
3600				$MarkRecord = $this->_getMarkRecord($MarkArray, $MarkPos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3601				//Mark Class is = $MarkRecord['Class']
3602				// Get the relevant LigatureRecord
3603				$this->seek($LigatureArray);
3604				$LigatureCount = $this->read_ushort();
3605				$LigaturePos = strpos($LigatureGlyphs, $this->OTLdata[$matchedpos]['hex']) / 6;
3606
3607				// Move to the LigatureAttach table Record we want
3608				$nSkip = (2 * $LigaturePos);
3609				$this->skip($nSkip);
3610				$LigatureAttachOffset = $LigatureArray + $this->read_ushort();
3611				$this->seek($LigatureAttachOffset);
3612				$ComponentCount = $this->read_ushort();
3613				$offsets = [];
3614				for ($comp = 0; $comp < $ComponentCount; $comp++) {
3615					// ComponentRecords
3616					for ($class = 0; $class < $ClassCount; $class++) {
3617						$offsets[$comp][$class] = $this->read_ushort();
3618					}
3619				}
3620
3621				// Get the specific component for this mark attachment
3622				if (isset($this->assocLigs[$matchedpos]) && isset($this->assocMarks[$ptr]['ligPos']) && $this->assocMarks[$ptr]['ligPos'] == $matchedpos) {
3623					$component = $this->assocMarks[$ptr]['compID'];
3624				} else {
3625					$component = $ComponentCount - 1;
3626				}
3627
3628				$offset = $offsets[$component][$MarkRecord['Class']];
3629				if ($offset != 0) {
3630					$LigatureRecordOffset = $offset + $LigatureAttachOffset;
3631					list($x, $y) = $this->_getAnchorTable($LigatureRecordOffset);
3632					$LigatureRecord = ['AnchorX' => $x, 'AnchorY' => $y];
3633
3634					// Need default XAdvance for Ligature glyph
3635					$LigatureWidth = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3636					$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $LigatureWidth;
3637					// And any intervening (ignored)characters
3638					if (($ptr - $matchedpos) > 1) {
3639						for ($i = $matchedpos + 1; $i < $ptr; $i++) {
3640							$LigatureWidthExtra = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$i]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3641							$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] += $LigatureWidthExtra;
3642						}
3643					}
3644
3645					// Align to previous Ligature by attachment - so need to add to previous placement values
3646					if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'])) {
3647						$prevXPlacement = $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'];
3648					} else {
3649						$prevXPlacement = 0;
3650					}
3651					if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'])) {
3652						$prevYPlacement = $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'];
3653					} else {
3654						$prevYPlacement = 0;
3655					}
3656
3657					$this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $LigatureRecord['AnchorX'] - $MarkRecord['AnchorX'];
3658					$this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $LigatureRecord['AnchorY'] - $MarkRecord['AnchorY'];
3659					if ($this->debugOTL) {
3660						$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3661					}
3662					return 1;
3663				}
3664			}
3665			return 0;
3666		} ////////////////////////////////////////////////////////////////////////////////
3667		// LookupType 6: MarkToMark attachment  Attach a combining mark to another mark
3668		////////////////////////////////////////////////////////////////////////////////
3669		elseif ($Type == 6) {
3670			$Mark1Coverage = $subtable_offset + $this->read_ushort(); // Combining Mark
3671			//$Mark1Coverage is already set in $LuCoverage 0065|0073 etc
3672			$Mark2Coverage = $subtable_offset + $this->read_ushort(); // Base Mark
3673			$ClassCount = $this->read_ushort(); // Number of classes defined for marks = No. of Combining mark1 glyphs in the MarkCoverage table
3674			$Mark1Array = $subtable_offset + $this->read_ushort(); // Offset to MarkArray table
3675			$Mark2Array = $subtable_offset + $this->read_ushort(); // Offset to Mark2Array table
3676			$this->seek($Mark2Coverage);
3677			$Mark2Glyphs = implode('|', $this->_getCoverage());
3678			$checkpos = $ptr;
3679			$checkpos--;
3680			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
3681				$checkpos--;
3682			}
3683			if (isset($this->OTLdata[$checkpos]) && strpos($Mark2Glyphs, $this->OTLdata[$checkpos]['hex']) !== false) {
3684				$matchedpos = $checkpos;
3685			} else {
3686				$matchedpos = false;
3687			}
3688
3689			if ($matchedpos !== false) {
3690				// Get the relevant MarkRecord
3691				$Mark1Pos = $LuCoverage[$currGID];
3692				$Mark1Record = $this->_getMarkRecord($Mark1Array, $Mark1Pos); // e.g. Array ( [Class] => 0 [AnchorX] => -549 [AnchorY] => 1548 )
3693				//Mark Class is = $Mark1Record['Class']
3694				// Get the relevant Mark2Record
3695				$this->seek($Mark2Array);
3696				$Mark2Count = $this->read_ushort();
3697				$Mark2Pos = strpos($Mark2Glyphs, $this->OTLdata[$matchedpos]['hex']) / 6;
3698
3699				// Move to the Mark2Record we want
3700				$nSkip = (2 * $Mark2Pos * $ClassCount );
3701				$this->skip($nSkip);
3702
3703				// Read Mark2Record we want for appropriate Class
3704				$nSkip = 2 * $Mark1Record['Class'];
3705				$this->skip($nSkip);
3706				$Mark2RecordOffset = $Mark2Array + $this->read_ushort();
3707				list($x, $y) = $this->_getAnchorTable($Mark2RecordOffset);
3708				$Mark2Record = ['AnchorX' => $x, 'AnchorY' => $y]; // e.g. Array ( [AnchorX] => 660 [AnchorY] => 1556 )
3709				// Need default XAdvance for Mark2 glyph
3710				$Mark2Width = $this->mpdf->_getCharWidth($this->mpdf->CurrentFont['cw'], $this->OTLdata[$matchedpos]['uni']) * $this->mpdf->CurrentFont['unitsPerEm'] / 1000; // convert back to font design units
3711				// IF combining marks are set on different components of a ligature glyph, do not apply this rule
3712				// Test: arabictypesetting: &#x625;&#x650;&#x644;&#x64e;&#x649;&#x670;&#x653;
3713				// Test: arabictypesetting: &#x628;&#x651;&#x64e;&#x64a;&#x652;&#x646;&#x64e;&#x643;&#x64f;&#x645;&#x652;
3714				$prevLig = -1;
3715				$thisLig = -1;
3716				$prevComp = -1;
3717				$thisComp = -1;
3718				if (isset($this->assocMarks[$matchedpos])) {
3719					$prevLig = $this->assocMarks[$matchedpos]['ligPos'];
3720					$prevComp = $this->assocMarks[$matchedpos]['compID'];
3721				}
3722				if (isset($this->assocMarks[$ptr])) {
3723					$thisLig = $this->assocMarks[$ptr]['ligPos'];
3724					$thisComp = $this->assocMarks[$ptr]['compID'];
3725				}
3726
3727				// However IF Mark2 (first in logical order, i.e. being attached to) is not associated with a base, carry on
3728				// 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
3729				if (!defined("OMIT_OTL_FIX_1") || OMIT_OTL_FIX_1 != 1) {
3730					/* OTL_FIX_1 */
3731					if (isset($this->assocMarks[$matchedpos]) && ($prevLig != $thisLig || $prevComp != $thisComp )) {
3732						return 0;
3733					}
3734				} else {
3735					/* Original code */
3736					if ($prevLig != $thisLig || $prevComp != $thisComp) {
3737						return 0;
3738					}
3739				}
3740
3741
3742				if (!defined("OMIT_OTL_FIX_2") || OMIT_OTL_FIX_2 != 1) {
3743					/* OTL_FIX_2 */
3744					if (!isset($this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) || !$this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) {
3745						$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $Mark2Width;
3746					}
3747				}
3748
3749				// ZZZ99Q - Test Case font-family: garuda &#xe19;&#xe49;&#xe33;
3750				if (isset($this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) && $this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth']) {
3751					$this->OTLdata[$ptr]['GPOSinfo']['BaseWidth'] = $this->OTLdata[$matchedpos]['GPOSinfo']['BaseWidth'];
3752				}
3753
3754				// Align to previous Mark by attachment - so need to add the previous placement values
3755				$prevXPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['XPlacement'] : 0);
3756				$prevYPlacement = (isset($this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement']) ? $this->OTLdata[$matchedpos]['GPOSinfo']['YPlacement'] : 0);
3757				$this->OTLdata[$ptr]['GPOSinfo']['XPlacement'] = $prevXPlacement + $Mark2Record['AnchorX'] - $Mark1Record['AnchorX'];
3758				$this->OTLdata[$ptr]['GPOSinfo']['YPlacement'] = $prevYPlacement + $Mark2Record['AnchorY'] - $Mark1Record['AnchorY'];
3759				if ($this->debugOTL) {
3760					$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3761				}
3762				return 1;
3763			}
3764			return 0;
3765		} ////////////////////////////////////////////////////////////////////////////////
3766		// LookupType 7: Context positioning    Position one or more glyphs in context
3767		////////////////////////////////////////////////////////////////////////////////
3768		elseif ($Type == 7) {
3769			//===========
3770			// Format 1:
3771			//===========
3772			if ($PosFormat == 1) {
3773				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3774			} //===========
3775			// Format 2:
3776			//===========
3777			elseif ($PosFormat == 2) {
3778				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
3779				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
3780				$PosClassSetCnt = $this->read_ushort();
3781				$PosClassSetOffset = [];
3782				for ($b = 0; $b < $PosClassSetCnt; $b++) {
3783					$offset = $this->read_ushort();
3784					if ($offset == 0x0000) {
3785						$PosClassSetOffset[] = $offset;
3786					} else {
3787						$PosClassSetOffset[] = $subtable_offset + $offset;
3788					}
3789				}
3790
3791				$InputClasses = $this->_getClasses($InputClassDefOffset);
3792
3793				for ($s = 0; $s < $PosClassSetCnt; $s++) { // $ChainPosClassSet is ordered by input class-may be NULL
3794					// Select $PosClassSet if currGlyph is in First Input Class
3795					if ($PosClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
3796						$this->seek($PosClassSetOffset[$s]);
3797						$PosClassRuleCnt = $this->read_ushort();
3798						$PosClassRule = [];
3799						for ($b = 0; $b < $PosClassRuleCnt; $b++) {
3800							$PosClassRule[$b] = $PosClassSetOffset[$s] + $this->read_ushort();
3801						}
3802
3803						for ($b = 0; $b < $PosClassRuleCnt; $b++) {  // EACH RULE
3804							$this->seek($PosClassRule[$b]);
3805							$InputGlyphCount = $this->read_ushort();
3806							$PosCount = $this->read_ushort();
3807
3808							$Input = [];
3809							for ($r = 1; $r < $InputGlyphCount; $r++) {
3810								$Input[$r] = $this->read_ushort();
3811							}
3812							$inputClass = $s;
3813
3814							$inputGlyphs = [];
3815							$inputGlyphs[0] = $InputClasses[$inputClass];
3816
3817							if ($InputGlyphCount > 1) {
3818								//  NB starts at 1
3819								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
3820									$classindex = $Input[$gcl];
3821									if (isset($InputClasses[$classindex])) {
3822										$inputGlyphs[$gcl] = $InputClasses[$classindex];
3823									} else {
3824										$inputGlyphs[$gcl] = '';
3825									}
3826								}
3827							}
3828
3829							// Class 0 contains all the glyphs NOT in the other classes
3830							$class0excl = [];
3831							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
3832								if (is_array($InputClasses[$gc])) {
3833									$class0excl = $class0excl + $InputClasses[$gc];
3834								}
3835							}
3836
3837							$backtrackGlyphs = [];
3838							$lookaheadGlyphs = [];
3839
3840							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl);
3841							if ($matched) {
3842								for ($p = 0; $p < $PosCount; $p++) { // EACH LOOKUP
3843									$SequenceIndex[$p] = $this->read_ushort();
3844									$LookupListIndex[$p] = $this->read_ushort();
3845								}
3846
3847								for ($p = 0; $p < $PosCount; $p++) {
3848									// Apply  $LookupListIndex  at   $SequenceIndex
3849									if ($SequenceIndex[$p] >= $InputGlyphCount) {
3850										continue;
3851									}
3852									$lu = $LookupListIndex[$p];
3853									$luType = $this->GPOSLookups[$lu]['Type'];
3854									$luFlag = $this->GPOSLookups[$lu]['Flag'];
3855									$luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
3856
3857									$luptr = $matched[$SequenceIndex[$p]];
3858									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
3859									$lucurrGID = $this->OTLdata[$luptr]['uni'];
3860
3861									foreach ($this->GPOSLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
3862										$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);
3863										if ($this->debugOTL && $shift) {
3864											$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
3865										}
3866										if ($shift) {
3867											break;
3868										}
3869									}
3870								}
3871
3872								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
3873									return $shift;
3874								} /* OTL_FIX_3 */
3875								else {
3876									return $InputGlyphCount; // should be + matched ignores in Input Sequence
3877								}
3878							}
3879						}
3880					}
3881				}
3882
3883				return 0;
3884			} //===========
3885			// Format 3:
3886			//===========
3887			elseif ($PosFormat == 3) {
3888				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3889			} else {
3890				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . ", Format " . $PosFormat . " not supported.");
3891			}
3892		} ////////////////////////////////////////////////////////////////////////////////
3893		// LookupType 8: Chained Context positioning    Position one or more glyphs in chained context
3894		////////////////////////////////////////////////////////////////////////////////
3895		elseif ($Type == 8) {
3896			//===========
3897			// Format 1:
3898			//===========
3899			if ($PosFormat == 1) {
3900				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " Format " . $PosFormat . " not TESTED YET.");
3901				return 0;
3902			} //===========
3903			// Format 2:
3904			//===========
3905			elseif ($PosFormat == 2) {
3906				$CoverageTableOffset = $subtable_offset + $this->read_ushort();
3907				$BacktrackClassDefOffset = $subtable_offset + $this->read_ushort();
3908				$InputClassDefOffset = $subtable_offset + $this->read_ushort();
3909				$LookaheadClassDefOffset = $subtable_offset + $this->read_ushort();
3910				$ChainPosClassSetCnt = $this->read_ushort();
3911				$ChainPosClassSetOffset = [];
3912				for ($b = 0; $b < $ChainPosClassSetCnt; $b++) {
3913					$offset = $this->read_ushort();
3914					if ($offset == 0x0000) {
3915						$ChainPosClassSetOffset[] = $offset;
3916					} else {
3917						$ChainPosClassSetOffset[] = $subtable_offset + $offset;
3918					}
3919				}
3920
3921				$BacktrackClasses = $this->_getClasses($BacktrackClassDefOffset);
3922				$InputClasses = $this->_getClasses($InputClassDefOffset);
3923				$LookaheadClasses = $this->_getClasses($LookaheadClassDefOffset);
3924
3925				for ($s = 0; $s < $ChainPosClassSetCnt; $s++) { // $ChainPosClassSet is ordered by input class-may be NULL
3926					// Select $ChainPosClassSet if currGlyph is in First Input Class
3927					if ($ChainPosClassSetOffset[$s] > 0 && isset($InputClasses[$s][$currGID])) {
3928						$this->seek($ChainPosClassSetOffset[$s]);
3929						$ChainPosClassRuleCnt = $this->read_ushort();
3930						$ChainPosClassRule = [];
3931						for ($b = 0; $b < $ChainPosClassRuleCnt; $b++) {
3932							$ChainPosClassRule[$b] = $ChainPosClassSetOffset[$s] + $this->read_ushort();
3933						}
3934
3935						for ($b = 0; $b < $ChainPosClassRuleCnt; $b++) {  // EACH RULE
3936							$this->seek($ChainPosClassRule[$b]);
3937							$BacktrackGlyphCount = $this->read_ushort();
3938							$Backtrack = [];
3939							for ($r = 0; $r < $BacktrackGlyphCount; $r++) {
3940								$Backtrack[$r] = $this->read_ushort();
3941							}
3942							$InputGlyphCount = $this->read_ushort();
3943							$Input = [];
3944							for ($r = 1; $r < $InputGlyphCount; $r++) {
3945								$Input[$r] = $this->read_ushort();
3946							}
3947							$LookaheadGlyphCount = $this->read_ushort();
3948							$Lookahead = [];
3949							for ($r = 0; $r < $LookaheadGlyphCount; $r++) {
3950								$Lookahead[$r] = $this->read_ushort();
3951							}
3952
3953							$inputClass = $s; //???
3954
3955							$inputGlyphs = [];
3956							$inputGlyphs[0] = $InputClasses[$inputClass];
3957
3958							if ($InputGlyphCount > 1) {
3959								//  NB starts at 1
3960								for ($gcl = 1; $gcl < $InputGlyphCount; $gcl++) {
3961									$classindex = $Input[$gcl];
3962									if (isset($InputClasses[$classindex])) {
3963										$inputGlyphs[$gcl] = $InputClasses[$classindex];
3964									} else {
3965										$inputGlyphs[$gcl] = '';
3966									}
3967								}
3968							}
3969
3970							// Class 0 contains all the glyphs NOT in the other classes
3971							$class0excl = [];
3972							for ($gc = 1; $gc <= count($InputClasses); $gc++) {
3973								if (isset($InputClasses[$gc]) && is_array($InputClasses[$gc])) {
3974									$class0excl = $class0excl + $InputClasses[$gc];
3975								}
3976							}
3977
3978							if ($BacktrackGlyphCount) {
3979								$backtrackGlyphs = [];
3980								for ($gcl = 0; $gcl < $BacktrackGlyphCount; $gcl++) {
3981									$classindex = $Backtrack[$gcl];
3982									if (isset($BacktrackClasses[$classindex])) {
3983										$backtrackGlyphs[$gcl] = $BacktrackClasses[$classindex];
3984									} else {
3985										$backtrackGlyphs[$gcl] = '';
3986									}
3987								}
3988							} else {
3989								$backtrackGlyphs = [];
3990							}
3991
3992							// Class 0 contains all the glyphs NOT in the other classes
3993							$bclass0excl = [];
3994							for ($gc = 1; $gc <= count($BacktrackClasses); $gc++) {
3995								if (isset($BacktrackClasses[$gc]) && is_array($BacktrackClasses[$gc])) {
3996									$bclass0excl = $bclass0excl + $BacktrackClasses[$gc];
3997								}
3998							}
3999
4000							if ($LookaheadGlyphCount) {
4001								$lookaheadGlyphs = [];
4002								for ($gcl = 0; $gcl < $LookaheadGlyphCount; $gcl++) {
4003									$classindex = $Lookahead[$gcl];
4004									if (isset($LookaheadClasses[$classindex])) {
4005										$lookaheadGlyphs[$gcl] = $LookaheadClasses[$classindex];
4006									} else {
4007										$lookaheadGlyphs[$gcl] = '';
4008									}
4009								}
4010							} else {
4011								$lookaheadGlyphs = [];
4012							}
4013
4014							// Class 0 contains all the glyphs NOT in the other classes
4015							$lclass0excl = [];
4016							for ($gc = 1; $gc <= count($LookaheadClasses); $gc++) {
4017								if (isset($LookaheadClasses[$gc]) && is_array($LookaheadClasses[$gc])) {
4018									$lclass0excl = $lclass0excl + $LookaheadClasses[$gc];
4019								}
4020							}
4021
4022							$matched = $this->checkContextMatchMultipleUni($inputGlyphs, $backtrackGlyphs, $lookaheadGlyphs, $ignore, $ptr, $class0excl, $bclass0excl, $lclass0excl);
4023							if ($matched) {
4024								$PosCount = $this->read_ushort();
4025								$SequenceIndex = [];
4026								$LookupListIndex = [];
4027								for ($p = 0; $p < $PosCount; $p++) { // EACH LOOKUP
4028									$SequenceIndex[$p] = $this->read_ushort();
4029									$LookupListIndex[$p] = $this->read_ushort();
4030								}
4031
4032								for ($p = 0; $p < $PosCount; $p++) {
4033									// Apply  $LookupListIndex  at   $SequenceIndex
4034									if ($SequenceIndex[$p] >= $InputGlyphCount) {
4035										continue;
4036									}
4037									$lu = $LookupListIndex[$p];
4038									$luType = $this->GPOSLookups[$lu]['Type'];
4039									$luFlag = $this->GPOSLookups[$lu]['Flag'];
4040									$luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
4041
4042									$luptr = $matched[$SequenceIndex[$p]];
4043									$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
4044									$lucurrGID = $this->OTLdata[$luptr]['uni'];
4045
4046									foreach ($this->GPOSLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
4047										$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);
4048										if ($this->debugOTL && $shift) {
4049											$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
4050										}
4051										if ($shift) {
4052											break;
4053										}
4054									}
4055								}
4056
4057								if (!defined("OMIT_OTL_FIX_3") || OMIT_OTL_FIX_3 != 1) {
4058									return $shift;
4059								} /* OTL_FIX_3 */
4060								else {
4061									return $InputGlyphCount; // should be + matched ignores in Input Sequence
4062								}
4063							}
4064						}
4065					}
4066				}
4067
4068				return 0;
4069			} //===========
4070			// Format 3:
4071			//===========
4072			elseif ($PosFormat == 3) {
4073				$BacktrackGlyphCount = $this->read_ushort();
4074				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
4075					$CoverageBacktrackOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
4076				}
4077				$InputGlyphCount = $this->read_ushort();
4078				for ($b = 0; $b < $InputGlyphCount; $b++) {
4079					$CoverageInputOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
4080				}
4081				$LookaheadGlyphCount = $this->read_ushort();
4082				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
4083					$CoverageLookaheadOffset[] = $subtable_offset + $this->read_ushort(); // in glyph sequence order
4084				}
4085				$PosCount = $this->read_ushort();
4086				$save_pos = $this->_pos; // Save the point just after PosCount
4087
4088				$CoverageBacktrackGlyphs = [];
4089				for ($b = 0; $b < $BacktrackGlyphCount; $b++) {
4090					$this->seek($CoverageBacktrackOffset[$b]);
4091					$glyphs = $this->_getCoverage();
4092					$CoverageBacktrackGlyphs[$b] = implode("|", $glyphs);
4093				}
4094				$CoverageInputGlyphs = [];
4095				for ($b = 0; $b < $InputGlyphCount; $b++) {
4096					$this->seek($CoverageInputOffset[$b]);
4097					$glyphs = $this->_getCoverage();
4098					$CoverageInputGlyphs[$b] = implode("|", $glyphs);
4099				}
4100				$CoverageLookaheadGlyphs = [];
4101				for ($b = 0; $b < $LookaheadGlyphCount; $b++) {
4102					$this->seek($CoverageLookaheadOffset[$b]);
4103					$glyphs = $this->_getCoverage();
4104					$CoverageLookaheadGlyphs[$b] = implode("|", $glyphs);
4105				}
4106				$matched = $this->checkContextMatchMultiple($CoverageInputGlyphs, $CoverageBacktrackGlyphs, $CoverageLookaheadGlyphs, $ignore, $ptr);
4107				if ($matched) {
4108					$this->seek($save_pos); // Return to just after PosCount
4109					for ($p = 0; $p < $PosCount; $p++) {
4110						// PosLookupRecord
4111						$PosLookupRecord[$p]['SequenceIndex'] = $this->read_ushort();
4112						$PosLookupRecord[$p]['LookupListIndex'] = $this->read_ushort();
4113					}
4114					for ($p = 0; $p < $PosCount; $p++) {
4115						// Apply  $PosLookupRecord[$p]['LookupListIndex']  at   $PosLookupRecord[$p]['SequenceIndex']
4116						if ($PosLookupRecord[$p]['SequenceIndex'] >= $InputGlyphCount) {
4117							continue;
4118						}
4119						$lu = $PosLookupRecord[$p]['LookupListIndex'];
4120						$luType = $this->GPOSLookups[$lu]['Type'];
4121						$luFlag = $this->GPOSLookups[$lu]['Flag'];
4122						if (isset($this->GPOSLookups[$lu]['MarkFilteringSet'])) {
4123							$luMarkFilteringSet = $this->GPOSLookups[$lu]['MarkFilteringSet'];
4124						} else {
4125							$luMarkFilteringSet = '';
4126						}
4127
4128						$luptr = $matched[$PosLookupRecord[$p]['SequenceIndex']];
4129						$lucurrGlyph = $this->OTLdata[$luptr]['hex'];
4130						$lucurrGID = $this->OTLdata[$luptr]['uni'];
4131
4132						foreach ($this->GPOSLookups[$lu]['Subtables'] as $luc => $lusubtable_offset) {
4133							$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);
4134							if ($this->debugOTL && $shift) {
4135								$this->_dumpproc('GPOS', $lookupID, $subtable, $Type, $PosFormat, $ptr, $currGlyph, $level);
4136							}
4137							if ($shift) {
4138								break;
4139							}
4140						}
4141					}
4142				}
4143			} else {
4144				throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . ", Format " . $PosFormat . " not supported.");
4145			}
4146		} else {
4147			throw new \Mpdf\MpdfException("GPOS Lookup Type " . $Type . " not supported.");
4148		}
4149	}
4150
4151	//////////////////////////////////////////////////////////////////////////////////
4152	// GPOS / GSUB / GCOM (common) functions
4153	//////////////////////////////////////////////////////////////////////////////////
4154	private function checkContextMatch($Input, $Backtrack, $Lookahead, $ignore, $ptr)
4155	{
4156		// Input etc are single numbers - GSUB Format 6.1
4157		// Input starts with (1=>xxx)
4158		// return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
4159
4160		$current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
4161
4162		// BACKTRACK
4163		$checkpos = $ptr;
4164		for ($i = 0; $i < count($Backtrack); $i++) {
4165			$checkpos--;
4166			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4167				$checkpos--;
4168			}
4169			// If outside scope of current syllable - return no match
4170			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4171				return false;
4172			} elseif (!isset($this->OTLdata[$checkpos]) || $this->OTLdata[$checkpos]['uni'] != $Backtrack[$i]) {
4173				return false;
4174			}
4175		}
4176
4177		// INPUT
4178		$matched = [0 => $ptr];
4179		$checkpos = $ptr;
4180		for ($i = 1; $i < count($Input); $i++) {
4181			$checkpos++;
4182			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4183				$checkpos++;
4184			}
4185			// If outside scope of current syllable - return no match
4186			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4187				return false;
4188			} elseif (isset($this->OTLdata[$checkpos]) && $this->OTLdata[$checkpos]['uni'] == $Input[$i]) {
4189				$matched[] = $checkpos;
4190			} else {
4191				return false;
4192			}
4193		}
4194
4195		// LOOKAHEAD
4196		for ($i = 0; $i < count($Lookahead); $i++) {
4197			$checkpos++;
4198			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4199				$checkpos++;
4200			}
4201			// If outside scope of current syllable - return no match
4202			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4203				return false;
4204			} elseif (!isset($this->OTLdata[$checkpos]) || $this->OTLdata[$checkpos]['uni'] != $Lookahead[$i]) {
4205				return false;
4206			}
4207		}
4208
4209		return $matched;
4210	}
4211
4212	private function checkContextMatchMultiple($Input, $Backtrack, $Lookahead, $ignore, $ptr, $class0excl = '', $bclass0excl = '', $lclass0excl = '')
4213	{
4214		// 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
4215		// Input starts with (1=>xxx)
4216		// return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
4217		// $class0excl is the string of glyphs in all classes except Class 0 (GSUB 5.2, 6.2, GPOS 7.2, 8.2)
4218		// $bclass0excl & $lclass0excl are the same for lookahead and backtrack (GSUB 6.2, GPOS 8.2)
4219
4220		$current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
4221
4222		// BACKTRACK
4223		$checkpos = $ptr;
4224		for ($i = 0; $i < count($Backtrack); $i++) {
4225			$checkpos--;
4226			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4227				$checkpos--;
4228			}
4229			// If outside scope of current syllable - return no match
4230			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4231				return false;
4232			} // If Class 0 specified, matches anything NOT in $bclass0excl
4233			elseif (!$Backtrack[$i] && isset($this->OTLdata[$checkpos]) && strpos($bclass0excl, $this->OTLdata[$checkpos]['hex']) !== false) {
4234				return false;
4235			} elseif (!isset($this->OTLdata[$checkpos]) || strpos($Backtrack[$i], $this->OTLdata[$checkpos]['hex']) === false) {
4236				return false;
4237			}
4238		}
4239
4240		// INPUT
4241		$matched = [0 => $ptr];
4242		$checkpos = $ptr;
4243		for ($i = 1; $i < count($Input); $i++) { // Start at 1 - already matched the first InputGlyph
4244			$checkpos++;
4245			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4246				$checkpos++;
4247			}
4248			// If outside scope of current syllable - return no match
4249			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4250				return false;
4251			} // If Input Class 0 specified, matches anything NOT in $class0excl
4252			elseif (!$Input[$i] && isset($this->OTLdata[$checkpos]) && strpos($class0excl, $this->OTLdata[$checkpos]['hex']) === false) {
4253				$matched[] = $checkpos;
4254			} elseif (isset($this->OTLdata[$checkpos]) && strpos($Input[$i], $this->OTLdata[$checkpos]['hex']) !== false) {
4255				$matched[] = $checkpos;
4256			} else {
4257				return false;
4258			}
4259		}
4260
4261		// LOOKAHEAD
4262		for ($i = 0; $i < count($Lookahead); $i++) {
4263			$checkpos++;
4264			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4265				$checkpos++;
4266			}
4267			// If outside scope of current syllable - return no match
4268			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4269				return false;
4270			} // If Class 0 specified, matches anything NOT in $lclass0excl
4271			elseif (!$Lookahead[$i] && isset($this->OTLdata[$checkpos]) && strpos($lclass0excl, $this->OTLdata[$checkpos]['hex']) !== false) {
4272				return false;
4273			} elseif (!isset($this->OTLdata[$checkpos]) || strpos($Lookahead[$i], $this->OTLdata[$checkpos]['hex']) === false) {
4274				return false;
4275			}
4276		}
4277		return $matched;
4278	}
4279
4280	private function checkContextMatchMultipleUni($Input, $Backtrack, $Lookahead, $ignore, $ptr, $class0excl = [], $bclass0excl = [], $lclass0excl = [])
4281	{
4282		// 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
4283		// Input starts with (1=>xxx)
4284		// return false if no match, else an array of ptr for matches (0=>0, 1=>3,...)
4285		// $class0excl is array of glyphs in all classes except Class 0 (GSUB 5.2, 6.2, GPOS 7.2, 8.2)
4286		// $bclass0excl & $lclass0excl are the same for lookahead and backtrack (GSUB 6.2, GPOS 8.2)
4287
4288		$current_syllable = (isset($this->OTLdata[$ptr]['syllable']) ? $this->OTLdata[$ptr]['syllable'] : 0);
4289
4290		// BACKTRACK
4291		$checkpos = $ptr;
4292		for ($i = 0; $i < count($Backtrack); $i++) {
4293			$checkpos--;
4294			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4295				$checkpos--;
4296			}
4297			// If outside scope of current syllable - return no match
4298			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4299				return false;
4300			} // If Class 0 specified, matches anything NOT in $bclass0excl
4301			elseif (!$Backtrack[$i] && isset($this->OTLdata[$checkpos]) && isset($bclass0excl[$this->OTLdata[$checkpos]['uni']])) {
4302				return false;
4303			} elseif (!isset($this->OTLdata[$checkpos]) || !isset($Backtrack[$i][$this->OTLdata[$checkpos]['uni']])) {
4304				return false;
4305			}
4306		}
4307
4308		// INPUT
4309		$matched = [0 => $ptr];
4310		$checkpos = $ptr;
4311		for ($i = 1; $i < count($Input); $i++) { // Start at 1 - already matched the first InputGlyph
4312			$checkpos++;
4313			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4314				$checkpos++;
4315			}
4316			// If outside scope of current syllable - return no match
4317			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4318				return false;
4319			} // If Input Class 0 specified, matches anything NOT in $class0excl
4320			elseif (!$Input[$i] && isset($this->OTLdata[$checkpos]) && !isset($class0excl[$this->OTLdata[$checkpos]['uni']])) {
4321				$matched[] = $checkpos;
4322			} elseif (isset($this->OTLdata[$checkpos]) && isset($Input[$i][$this->OTLdata[$checkpos]['uni']])) {
4323				$matched[] = $checkpos;
4324			} else {
4325				return false;
4326			}
4327		}
4328
4329		// LOOKAHEAD
4330		for ($i = 0; $i < count($Lookahead); $i++) {
4331			$checkpos++;
4332			while (isset($this->OTLdata[$checkpos]) && strpos($ignore, $this->OTLdata[$checkpos]['hex']) !== false) {
4333				$checkpos++;
4334			}
4335			// If outside scope of current syllable - return no match
4336			if ($this->restrictToSyllable && isset($this->OTLdata[$checkpos]['syllable']) && $this->OTLdata[$checkpos]['syllable'] != $current_syllable) {
4337				return false;
4338			} // If Class 0 specified, matches anything NOT in $lclass0excl
4339			elseif (!$Lookahead[$i] && isset($this->OTLdata[$checkpos]) && isset($lclass0excl[$this->OTLdata[$checkpos]['uni']])) {
4340				return false;
4341			} elseif (!isset($this->OTLdata[$checkpos]) || !isset($Lookahead[$i][$this->OTLdata[$checkpos]['uni']])) {
4342				return false;
4343			}
4344		}
4345		return $matched;
4346	}
4347
4348	private function _getClassDefinitionTable($offset)
4349	{
4350		if (isset($this->LuDataCache[$this->fontkey][$offset])) {
4351			$GlyphByClass = $this->LuDataCache[$this->fontkey][$offset];
4352		} else {
4353			$this->seek($offset);
4354			$ClassFormat = $this->read_ushort();
4355			$GlyphClass = [];
4356			//      $GlyphByClass = array(0=>array());  // NB This forces an index[0]
4357			if ($ClassFormat == 1) {
4358				$StartGlyph = $this->read_ushort();
4359				$GlyphCount = $this->read_ushort();
4360				for ($i = 0; $i < $GlyphCount; $i++) {
4361					$GlyphClass[$i]['startGlyphID'] = $StartGlyph + $i;
4362					$GlyphClass[$i]['endGlyphID'] = $StartGlyph + $i;
4363					$GlyphClass[$i]['class'] = $this->read_ushort();
4364					for ($g = $GlyphClass[$i]['startGlyphID']; $g <= $GlyphClass[$i]['endGlyphID']; $g++) {
4365						$GlyphByClass[$GlyphClass[$i]['class']][] = $this->glyphToChar($g);
4366					}
4367				}
4368			} elseif ($ClassFormat == 2) {
4369				$tableCount = $this->read_ushort();
4370				for ($i = 0; $i < $tableCount; $i++) {
4371					$GlyphClass[$i]['startGlyphID'] = $this->read_ushort();
4372					$GlyphClass[$i]['endGlyphID'] = $this->read_ushort();
4373					$GlyphClass[$i]['class'] = $this->read_ushort();
4374					for ($g = $GlyphClass[$i]['startGlyphID']; $g <= $GlyphClass[$i]['endGlyphID']; $g++) {
4375						$GlyphByClass[$GlyphClass[$i]['class']][] = $this->glyphToChar($g);
4376					}
4377				}
4378			}
4379			ksort($GlyphByClass);
4380			$this->LuDataCache[$this->fontkey][$offset] = $GlyphByClass;
4381		}
4382		return $GlyphByClass;
4383	}
4384
4385	private function count_bits($n)
4386	{
4387		for ($c = 0; $n; $c++) {
4388			$n &= $n - 1; // clear the least significant bit set
4389		}
4390		return $c;
4391	}
4392
4393	private function _getValueRecord($ValueFormat)
4394	{
4395	// Common ValueRecord for GPOS
4396		// Only returns 3 possible: $vra['XPlacement'] $vra['YPlacement'] $vra['XAdvance']
4397		$vra = [];
4398		// Horizontal adjustment for placement - in design units
4399		if (($ValueFormat & 0x0001) == 0x0001) {
4400			$vra['XPlacement'] = $this->read_short();
4401		}
4402		// Vertical adjustment for placement - in design units
4403		if (($ValueFormat & 0x0002) == 0x0002) {
4404			$vra['YPlacement'] = $this->read_short();
4405		}
4406		// Horizontal adjustment for advance - in design units (only used for horizontal writing)
4407		if (($ValueFormat & 0x0004) == 0x0004) {
4408			$vra['XAdvance'] = $this->read_short();
4409		}
4410		// Vertical adjustment for advance - in design units (only used for vertical writing)
4411		if (($ValueFormat & 0x0008) == 0x0008) {
4412			$this->read_short();
4413		}
4414		// Offset to Device table for horizontal placement-measured from beginning of PosTable (may be NULL)
4415		if (($ValueFormat & 0x0010) == 0x0010) {
4416			$this->read_ushort();
4417		}
4418		// Offset to Device table for vertical placement-measured from beginning of PosTable (may be NULL)
4419		if (($ValueFormat & 0x0020) == 0x0020) {
4420			$this->read_ushort();
4421		}
4422		// Offset to Device table for horizontal advance-measured from beginning of PosTable (may be NULL)
4423		if (($ValueFormat & 0x0040) == 0x0040) {
4424			$this->read_ushort();
4425		}
4426		// Offset to Device table for vertical advance-measured from beginning of PosTable (may be NULL)
4427		if (($ValueFormat & 0x0080) == 0x0080) {
4428			$this->read_ushort();
4429		}
4430		return $vra;
4431	}
4432
4433	private function _getAnchorTable($offset = 0)
4434	{
4435		if ($offset) {
4436			$this->seek($offset);
4437		}
4438		$AnchorFormat = $this->read_ushort();
4439		$XCoordinate = $this->read_short();
4440		$YCoordinate = $this->read_short();
4441		// Format 2 specifies additional link to contour point; Format 3 additional Device table
4442		return [$XCoordinate, $YCoordinate];
4443	}
4444
4445	private function _getMarkRecord($offset, $MarkPos)
4446	{
4447		$this->seek($offset);
4448		$MarkCount = $this->read_ushort();
4449		$this->skip($MarkPos * 4);
4450		$Class = $this->read_ushort();
4451		$MarkAnchor = $offset + $this->read_ushort();  // = Offset to anchor table
4452		list($x, $y) = $this->_getAnchorTable($MarkAnchor);
4453		$MarkRecord = ['Class' => $Class, 'AnchorX' => $x, 'AnchorY' => $y];
4454		return $MarkRecord;
4455	}
4456
4457	private function _getGCOMignoreString($flag, $MarkFilteringSet)
4458	{
4459		// If ignoreFlag set, combine all ignore glyphs into -> "(?:( 0FBA1| 0FBA2| 0FBA3)*)"
4460		// else "()"
4461		// for Input - set on secondary Lookup table if in Context, and set Backtrack and Lookahead on Context Lookup
4462		$str = "";
4463		$ignoreflag = 0;
4464
4465		// Flag & 0xFF?? = MarkAttachmentType
4466		if ($flag & 0xFF00) {
4467			// "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
4468			// $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
4469			$MarkAttachmentType = $flag >> 8;
4470			$ignoreflag = $flag;
4471			$str = $this->MarkAttachmentType[$MarkAttachmentType];
4472		}
4473
4474		// Flag & 0x0010 = UseMarkFilteringSet
4475		if ($flag & 0x0010) {
4476			throw new \Mpdf\MpdfException("This font [" . $this->fontkey . "] contains MarkGlyphSets - Not tested yet");
4477			// Change also in ttfontsuni.php
4478			if ($MarkFilteringSet == '') {
4479				throw new \Mpdf\MpdfException("This font [" . $this->fontkey . "] contains MarkGlyphSets - but MarkFilteringSet not set");
4480			}
4481			$str = $this->MarkGlyphSets[$MarkFilteringSet];
4482		}
4483
4484		// If Ignore Marks set, supercedes any above
4485		// Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
4486		if (($flag & 0x0008) == 0x0008 && ($flag & 0xFF00) == 0) {
4487			$ignoreflag = 8;
4488			$str = $this->GlyphClassMarks;
4489		}
4490
4491		// Flag & 0x0004 = Ignore Ligatures
4492		if (($flag & 0x0004) == 0x0004) {
4493			$ignoreflag += 4;
4494			if ($str) {
4495				$str .= "|";
4496			}
4497			$str .= $this->GlyphClassLigatures;
4498		}
4499		// Flag & 0x0002 = Ignore BaseGlyphs
4500		if (($flag & 0x0002) == 0x0002) {
4501			$ignoreflag += 2;
4502			if ($str) {
4503				$str .= "|";
4504			}
4505			$str .= $this->GlyphClassBases;
4506		}
4507		if ($str) {
4508			return "((?:(?:" . $str . "))*)";
4509		} else {
4510			return "()";
4511		}
4512	}
4513
4514	private function _checkGCOMignore($flag, $glyph, $MarkFilteringSet)
4515	{
4516		$ignore = false;
4517		// Flag & 0x0008 = Ignore Marks - (unless already done with MarkAttachmentType)
4518		if (($flag & 0x0008 && ($flag & 0xFF00) == 0) && strpos($this->GlyphClassMarks, $glyph)) {
4519			$ignore = true;
4520		}
4521		if (($flag & 0x0004) && strpos($this->GlyphClassLigatures, $glyph)) {
4522			$ignore = true;
4523		}
4524		if (($flag & 0x0002) && strpos($this->GlyphClassBases, $glyph)) {
4525			$ignore = true;
4526		}
4527		// Flag & 0xFF?? = MarkAttachmentType
4528		if ($flag & 0xFF00) {
4529			// "a lookup must ignore any mark glyphs that are not in the specified mark attachment class"
4530			// $this->MarkAttachmentType is already adjusted for this i.e. contains all Marks except those in the MarkAttachmentClassDef table
4531			if (strpos($this->MarkAttachmentType[($flag >> 8)], $glyph)) {
4532				$ignore = true;
4533			}
4534		}
4535		// Flag & 0x0010 = UseMarkFilteringSet
4536		if (($flag & 0x0010) && strpos($this->MarkGlyphSets[$MarkFilteringSet], $glyph)) {
4537			$ignore = true;
4538		}
4539		return $ignore;
4540	}
4541
4542	/**
4543	 * Bidi algorithm
4544	 *
4545	 * These functions are called from mpdf after GSUB/GPOS has taken place
4546	 * At this stage the bidi-type is in string form
4547	 *
4548	 * Bidirectional Character Types
4549	 * =============================
4550	 * Type  Description     General Scope
4551	 * Strong
4552	 * L     Left-to-Right       LRM, most alphabetic, syllabic, Han ideographs, non-European or non-Arabic digits, ...
4553	 * LRE   Left-to-Right Embedding LRE
4554	 * LRO   Left-to-Right Override  LRO
4555	 * R     Right-to-Left       RLM, Hebrew alphabet, and related punctuation
4556	 * AL    Right-to-Left Arabic    Arabic, Thaana, and Syriac alphabets, most punctuation specific to those scripts, ...
4557	 * RLE   Right-to-Left Embedding RLE
4558	 * RLO   Right-to-Left Override  RLO
4559	 * Weak
4560	 * PDF   Pop Directional Format      PDF
4561	 * EN    European Number             European digits, Eastern Arabic-Indic digits, ...
4562	 * ES    European Number Separator   Plus sign, minus sign
4563	 * ET    European Number Terminator  Degree sign, currency symbols, ...
4564	 * AN    Arabic Number           Arabic-Indic digits, Arabic decimal and thousands separators, ...
4565	 * CS    Common Number Separator     Colon, comma, full stop (period), No-break space, ...
4566	 * NSM   Nonspacing Mark             Characters marked Mn (Nonspacing_Mark) and Me (Enclosing_Mark) in the Unicode Character Database
4567	 * BN    Boundary Neutral            Default ignorables, non-characters, and control characters, other than those explicitly given other types.
4568	 * Neutral
4569	 * B     Paragraph Separator     Paragraph separator, appropriate Newline Functions, higher-level protocol paragraph determination
4570	 * S     Segment Separator   Tab
4571	 * WS    Whitespace          Space, figure space, line separator, form feed, General Punctuation spaces, ...
4572	 * ON    Other Neutrals      All other characters, including OBJECT REPLACEMENT CHARACTER
4573	 */
4574	public function bidiSort($ta, $str, $dir, &$chunkOTLdata, $useGPOS)
4575	{
4576
4577		$pel = 0; // paragraph embedding level
4578		$maxlevel = 0;
4579		$numchars = count($chunkOTLdata['char_data']);
4580
4581		// Set the initial paragraph embedding level
4582		if ($dir == 'rtl') {
4583			$pel = 1;
4584		} else {
4585			$pel = 0;
4586		}
4587
4588		// X1. Begin by setting the current embedding level to the paragraph embedding level. Set the directional override status to neutral.
4589		// Current Embedding Level
4590		$cel = $pel;
4591		// directional override status (-1 is Neutral)
4592		$dos = -1;
4593		$remember = [];
4594
4595		// Array of characters data
4596		$chardata = [];
4597
4598		// Process each character iteratively, applying rules X2 through X9. Only embedding levels from 0 to 61 are valid in this phase.
4599		// In the resolution of levels in rules I1 and I2, the maximum embedding level of 62 can be reached.
4600		for ($i = 0; $i < $numchars; ++$i) {
4601			if ($chunkOTLdata['char_data'][$i]['uni'] == 8235) { // RLE
4602				// X2. With each RLE, compute the least greater odd embedding level.
4603				//  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.
4604				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4605				$next_level = $cel + ($cel % 2) + 1;
4606				if ($next_level < 62) {
4607					$remember[] = ['num' => 8235, 'cel' => $cel, 'dos' => $dos];
4608					$cel = $next_level;
4609					$dos = -1;
4610				}
4611			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8234) { // LRE
4612				// X3. With each LRE, compute the least greater even embedding level.
4613				//  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.
4614				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4615				$next_level = $cel + 2 - ($cel % 2);
4616				if ($next_level < 62) {
4617					$remember[] = ['num' => 8234, 'cel' => $cel, 'dos' => $dos];
4618					$cel = $next_level;
4619					$dos = -1;
4620				}
4621			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8238) { // RLO
4622				// X4. With each RLO, compute the least greater odd embedding level.
4623				//  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.
4624				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4625				$next_level = $cel + ($cel % 2) + 1;
4626				if ($next_level < 62) {
4627					$remember[] = ['num' => 8238, 'cel' => $cel, 'dos' => $dos];
4628					$cel = $next_level;
4629					$dos = Ucdn::BIDI_CLASS_R;
4630				}
4631			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8237) { // LRO
4632				// X5. With each LRO, compute the least greater even embedding level.
4633				//  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.
4634				//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
4635				$next_level = $cel + 2 - ($cel % 2);
4636				if ($next_level < 62) {
4637					$remember[] = ['num' => 8237, 'cel' => $cel, 'dos' => $dos];
4638					$cel = $next_level;
4639					$dos = Ucdn::BIDI_CLASS_L;
4640				}
4641			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8236) { // PDF
4642				// 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.
4643				if (count($remember)) {
4644					$last = count($remember) - 1;
4645					if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
4646						($remember[$last]['num'] == 8237)) {
4647						$match = array_pop($remember);
4648						$cel = $match['cel'];
4649						$dos = $match['dos'];
4650					}
4651				}
4652			} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 10) { // NEW LINE
4653				// Reset to start values
4654				$cel = $pel;
4655				$dos = -1;
4656				$remember = [];
4657			} else {
4658				// X6. For all types besides RLE, LRE, RLO, LRO, and PDF:
4659				//  a. Set the level of the current character to the current embedding level.
4660				//  b. When the directional override status is not neutral, reset the current character type to directional override status.
4661				if ($dos != -1) {
4662					$chardir = $dos;
4663				} else {
4664					$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
4665				}
4666				// stores string characters and other information
4667				if (isset($chunkOTLdata['GPOSinfo'][$i])) {
4668					$gpos = $chunkOTLdata['GPOSinfo'][$i];
4669				} else {
4670					$gpos = '';
4671				}
4672				$chardata[] = ['char' => $chunkOTLdata['char_data'][$i]['uni'], 'level' => $cel, 'type' => $chardir, 'group' => $chunkOTLdata['group']{$i}, 'GPOSinfo' => $gpos];
4673			}
4674		}
4675
4676		$numchars = count($chardata);
4677
4678		// X8. All explicit directional embeddings and overrides are completely terminated at the end of each paragraph.
4679		// Paragraph separators are not included in the embedding.
4680		// X9. Remove all RLE, LRE, RLO, LRO, and PDF codes.
4681		// This is effectively done by only saving other codes to chardata
4682		// 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:
4683		// 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.
4684		// 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.
4685		// If the higher level is odd, the sor or eor is R; otherwise, it is L.
4686
4687		$prelevel = $pel;
4688		$postlevel = $pel;
4689		$cel = $prelevel; // current embedding level
4690		for ($i = 0; $i < $numchars; ++$i) {
4691			$level = $chardata[$i]['level'];
4692			if ($i == 0) {
4693				$left = $prelevel;
4694			} else {
4695				$left = $chardata[$i - 1]['level'];
4696			}
4697			if ($i == ($numchars - 1)) {
4698				$right = $postlevel;
4699			} else {
4700				$right = $chardata[$i + 1]['level'];
4701			}
4702			$chardata[$i]['sor'] = max($left, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
4703			$chardata[$i]['eor'] = max($right, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
4704		}
4705
4706
4707
4708		// 3.3.3 Resolving Weak Types
4709		// 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.
4710		// Nonspacing marks are now resolved based on the previous characters.
4711		// 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.
4712		for ($i = 0; $i < $numchars; ++$i) {
4713			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_NSM) {
4714				if ($i == 0 || $chardata[$i]['level'] != $chardata[$i - 1]['level']) {
4715					$chardata[$i]['type'] = $chardata[$i]['sor'];
4716				} else {
4717					$chardata[$i]['type'] = $chardata[($i - 1)]['type'];
4718				}
4719			}
4720		}
4721
4722		// 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.
4723		$prevlevel = -1;
4724		$levcount = 0;
4725		for ($i = 0; $i < $numchars; ++$i) {
4726			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN) {
4727				$found = false;
4728				for ($j = $levcount; $j >= 0; $j--) {
4729					if ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_AL) {
4730						$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
4731						$found = true;
4732						break;
4733					} elseif (($chardata[$j]['type'] == Ucdn::BIDI_CLASS_L) || ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_R)) {
4734						$found = true;
4735						break;
4736					}
4737				}
4738			}
4739			if ($chardata[$i]['level'] != $prevlevel) {
4740				$levcount = 0;
4741			} else {
4742				++$levcount;
4743			}
4744			$prevlevel = $chardata[$i]['level'];
4745		}
4746
4747		// W3. Change all ALs to R.
4748		for ($i = 0; $i < $numchars; ++$i) {
4749			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL) {
4750				$chardata[$i]['type'] = Ucdn::BIDI_CLASS_R;
4751			}
4752		}
4753
4754		// 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.
4755		for ($i = 1; $i < $numchars; ++$i) {
4756			if (($i + 1) < $numchars && $chardata[($i)]['level'] == $chardata[($i + 1)]['level'] && $chardata[($i)]['level'] == $chardata[($i - 1)]['level']) {
4757				if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN && $chardata[($i + 1)]['type'] == Ucdn::BIDI_CLASS_EN) {
4758					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4759				} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN && $chardata[($i + 1)]['type'] == Ucdn::BIDI_CLASS_EN) {
4760					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4761				} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_AN && $chardata[($i + 1)]['type'] == Ucdn::BIDI_CLASS_AN) {
4762					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
4763				}
4764			}
4765		}
4766
4767		// W5. A sequence of European terminators adjacent to European numbers changes to all European numbers.
4768		for ($i = 0; $i < $numchars; ++$i) {
4769			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) {
4770				if ($i > 0 && $chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_EN && $chardata[($i)]['level'] == $chardata[($i - 1)]['level']) {
4771					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4772				} else {
4773					$j = $i + 1;
4774					while ($j < $numchars && $chardata[$j]['level'] == $chardata[$i]['level']) {
4775						if ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_EN) {
4776							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
4777							break;
4778						} elseif ($chardata[$j]['type'] != Ucdn::BIDI_CLASS_ET) {
4779							break;
4780						}
4781						++$j;
4782					}
4783				}
4784			}
4785		}
4786
4787		// W6. Otherwise, separators and terminators change to Other Neutral.
4788		for ($i = 0; $i < $numchars; ++$i) {
4789			if (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS)) {
4790				$chardata[$i]['type'] = Ucdn::BIDI_CLASS_ON;
4791			}
4792		}
4793
4794		//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.
4795		for ($i = 0; $i < $numchars; ++$i) {
4796			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN) {
4797				if ($i == 0) { // Start of Level run
4798					if ($chardata[$i]['sor'] == Ucdn::BIDI_CLASS_L) {
4799						$chardata[$i]['type'] = $chardata[$i]['sor'];
4800					}
4801				} else {
4802					for ($j = $i - 1; $j >= 0; $j--) {
4803						if ($chardata[$j]['level'] != $chardata[$i]['level']) { // Level run boundary
4804							if ($chardata[$j + 1]['sor'] == Ucdn::BIDI_CLASS_L) {
4805								$chardata[$i]['type'] = $chardata[$j + 1]['sor'];
4806							}
4807							break;
4808						} elseif ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_L) {
4809							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_L;
4810							break;
4811						} elseif ($chardata[$j]['type'] == Ucdn::BIDI_CLASS_R) {
4812							break;
4813						}
4814					}
4815				}
4816			}
4817		}
4818
4819		// 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.
4820		for ($i = 0; $i < $numchars; ++$i) {
4821			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS) {
4822				$left = -1;
4823				// LEFT
4824				if ($i == 0) {  // first char
4825					$left = $chardata[($i)]['sor'];
4826				} elseif ($chardata[($i - 1)]['level'] != $chardata[($i)]['level']) {  // run boundary
4827					$left = $chardata[($i)]['sor'];
4828				} elseif ($chardata[($i - 1)]['type'] == Ucdn::BIDI_CLASS_L) {
4829					$left = Ucdn::BIDI_CLASS_L;
4830				} 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) {
4831					$left = Ucdn::BIDI_CLASS_R;
4832				}
4833				// RIGHT
4834				$right = -1;
4835				$j = $i;
4836				// move to the right of any following neutrals OR hit a run boundary
4837				while (($chardata[$j]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$j]['type'] == Ucdn::BIDI_CLASS_WS) && $j <= ($numchars - 1)) {
4838					if ($j == ($numchars - 1)) {  // last char
4839						$right = $chardata[($j)]['eor'];
4840						break;
4841					} elseif ($chardata[($j + 1)]['level'] != $chardata[($j)]['level']) {  // run boundary
4842						$right = $chardata[($j)]['eor'];
4843						break;
4844					} elseif ($chardata[($j + 1)]['type'] == Ucdn::BIDI_CLASS_L) {
4845						$right = Ucdn::BIDI_CLASS_L;
4846						break;
4847					} 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) {
4848						$right = Ucdn::BIDI_CLASS_R;
4849						break;
4850					}
4851					$j++;
4852				}
4853				if ($left > -1 && $left == $right) {
4854					$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
4855					$chardata[$i]['type'] = $left;
4856				}
4857			}
4858		}
4859
4860		// N2. Any remaining neutrals take the embedding direction
4861		for ($i = 0; $i < $numchars; ++$i) {
4862			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS) {
4863				$chardata[$i]['type'] = ($chardata[$i]['level'] % 2) ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
4864				$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
4865			}
4866		}
4867
4868		// 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.
4869		// I2. For all characters with an odd (right-to-left) embedding direction, those of type L, EN or AN go up one level.
4870		for ($i = 0; $i < $numchars; ++$i) {
4871			$odd = $chardata[$i]['level'] % 2;
4872			if ($odd) {
4873				if (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
4874					$chardata[$i]['level'] += 1;
4875				}
4876			} else {
4877				if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_R) {
4878					$chardata[$i]['level'] += 1;
4879				} elseif (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
4880					$chardata[$i]['level'] += 2;
4881				}
4882			}
4883			$maxlevel = max($chardata[$i]['level'], $maxlevel);
4884		}
4885
4886		// NB
4887		//  Separate into lines at this point************
4888		//
4889		// L1. On each line, reset the embedding level of the following characters to the paragraph embedding level:
4890		//  1. Segment separators (Tab) 'S',
4891		//  2. Paragraph separators 'B',
4892		//  3. Any sequence of whitespace characters 'WS' preceding a segment separator or paragraph separator, and
4893		//  4. Any sequence of whitespace characters 'WS' at the end of the line.
4894		//  The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2*******
4895		//  Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line.
4896
4897		for ($i = ($numchars - 1); $i > 0; $i--) {
4898			if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS || (isset($chardata[$i]['orig_type']) && $chardata[$i]['orig_type'] == Ucdn::BIDI_CLASS_WS)) {
4899				$chardata[$i]['level'] = $pel;
4900			} else {
4901				break;
4902			}
4903		}
4904
4905
4906		// 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.
4907		for ($j = $maxlevel; $j > 0; $j--) {
4908			$ordarray = [];
4909			$revarr = [];
4910			$onlevel = false;
4911			for ($i = 0; $i < $numchars; ++$i) {
4912				if ($chardata[$i]['level'] >= $j) {
4913					$onlevel = true;
4914
4915					// 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.
4916					if (isset(Ucdn::$mirror_pairs[$chardata[$i]['char']]) && $chardata[$i]['type'] == Ucdn::BIDI_CLASS_R) {
4917						$chardata[$i]['char'] = Ucdn::$mirror_pairs[$chardata[$i]['char']];
4918					}
4919
4920					$revarr[] = $chardata[$i];
4921				} else {
4922					if ($onlevel) {
4923						$revarr = array_reverse($revarr);
4924						$ordarray = array_merge($ordarray, $revarr);
4925						$revarr = [];
4926						$onlevel = false;
4927					}
4928					$ordarray[] = $chardata[$i];
4929				}
4930			}
4931			if ($onlevel) {
4932				$revarr = array_reverse($revarr);
4933				$ordarray = array_merge($ordarray, $revarr);
4934			}
4935			$chardata = $ordarray;
4936		}
4937
4938		$group = '';
4939		$e = '';
4940		$GPOS = [];
4941		$cctr = 0;
4942		$rtl_content = 0x0;
4943		foreach ($chardata as $cd) {
4944			$e.=code2utf($cd['char']);
4945			$group .= $cd['group'];
4946			if ($useGPOS && is_array($cd['GPOSinfo'])) {
4947				$GPOS[$cctr] = $cd['GPOSinfo'];
4948				$GPOS[$cctr]['wDir'] = ($cd['level'] % 2) ? 'RTL' : 'LTR';
4949			}
4950			if ($cd['type'] == Ucdn::BIDI_CLASS_L) {
4951				$rtl_content |= 1;
4952			} elseif ($cd['type'] == Ucdn::BIDI_CLASS_R) {
4953				$rtl_content |= 2;
4954			}
4955			$cctr++;
4956		}
4957
4958
4959		$chunkOTLdata['group'] = $group;
4960		if ($useGPOS) {
4961			$chunkOTLdata['GPOSinfo'] = $GPOS;
4962		}
4963
4964		return [$e, $rtl_content];
4965	}
4966
4967	/**
4968	 * The following versions for BidiSort work on amalgamated chunks to process the whole paragraph
4969	 *
4970	 * Firstly set the level in the OTLdata - called from fn printbuffer() [_bidiPrepare]
4971	 * Secondly re-order - called from fn writeFlowingBlock and FinishFlowingBlock, when already divided into lines. [_bidiReorder]
4972	 */
4973	public function bidiPrepare(&$para, $dir)
4974	{
4975
4976		// Set the initial paragraph embedding level
4977		$pel = 0; // paragraph embedding level
4978		if ($dir == 'rtl') {
4979			$pel = 1;
4980		}
4981
4982		// X1. Begin by setting the current embedding level to the paragraph embedding level. Set the directional override status to neutral.
4983		// Current Embedding Level
4984		$cel = $pel;
4985		// directional override status (-1 is Neutral)
4986		$dos = -1;
4987		$remember = [];
4988		$controlchars = false;
4989		$strongrtl = false;
4990		$diid = 0; // direction isolate ID
4991		$dictr = 0; // direction isolate counter
4992		// Process each character iteratively, applying rules X2 through X9. Only embedding levels from 0 to 61 are valid in this phase.
4993		// In the resolution of levels in rules I1 and I2, the maximum embedding level of 62 can be reached.
4994		$numchunks = count($para);
4995		for ($nc = 0; $nc < $numchunks; $nc++) {
4996			$chunkOTLdata = & $para[$nc][18];
4997
4998			$numchars = count($chunkOTLdata['char_data']);
4999			for ($i = 0; $i < $numchars; ++$i) {
5000				if ($chunkOTLdata['char_data'][$i]['uni'] == 8235) { // RLE
5001					// X2. With each RLE, compute the least greater odd embedding level.
5002					//  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.
5003					//  b. If the new level would not be valid, then this code is invalid. Do not change the current level or override status.
5004					$next_level = $cel + ($cel % 2) + 1;
5005					if ($next_level < 62) {
5006						$remember[] = ['num' => 8235, 'cel' => $cel, 'dos' => $dos];
5007						$cel = $next_level;
5008						$dos = -1;
5009						$controlchars = true;
5010					}
5011				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8234) { // LRE
5012					// X3. With each LRE, compute the least greater even 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 + 2 - ($cel % 2);
5016					if ($next_level < 62) {
5017						$remember[] = ['num' => 8234, 'cel' => $cel, 'dos' => $dos];
5018						$cel = $next_level;
5019						$dos = -1;
5020						$controlchars = true;
5021					}
5022				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8238) { // RLO
5023					// X4. With each RLO, compute the least greater odd 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 right-to-left.
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 + ($cel % 2) + 1;
5027					if ($next_level < 62) {
5028						$remember[] = ['num' => 8238, 'cel' => $cel, 'dos' => $dos];
5029						$cel = $next_level;
5030						$dos = Ucdn::BIDI_CLASS_R;
5031						$controlchars = true;
5032					}
5033				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8237) { // LRO
5034					// X5. With each LRO, compute the least greater even 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 left-to-right.
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 + 2 - ($cel % 2);
5038					if ($next_level < 62) {
5039						$remember[] = ['num' => 8237, 'cel' => $cel, 'dos' => $dos];
5040						$cel = $next_level;
5041						$dos = Ucdn::BIDI_CLASS_L;
5042						$controlchars = true;
5043					}
5044				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8236) { // PDF
5045					// 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.
5046					if (count($remember)) {
5047						$last = count($remember) - 1;
5048						if (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
5049							($remember[$last]['num'] == 8237)) {
5050							$match = array_pop($remember);
5051							$cel = $match['cel'];
5052							$dos = $match['dos'];
5053						}
5054					}
5055				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8294 || $chunkOTLdata['char_data'][$i]['uni'] == 8295 ||
5056					$chunkOTLdata['char_data'][$i]['uni'] == 8296) { // LRI // RLI // FSI
5057					// X5a. With each RLI:
5058					// X5b. With each LRI:
5059					// X5c. With each FSI, apply rules P2 and P3 for First Strong character
5060					//  Set the RLI/LRI/FSI embedding level to the embedding level of the last entry on the directional status stack.
5061					if ($dos != -1) {
5062						$chardir = $dos;
5063					} else {
5064						$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
5065					}
5066					$chunkOTLdata['char_data'][$i]['level'] = $cel;
5067					$chunkOTLdata['char_data'][$i]['type'] = $chardir;
5068					$chunkOTLdata['char_data'][$i]['diid'] = $diid;
5069
5070					$fsi = '';
5071					// X5c. With each FSI, apply rules P2 and P3 within the isolate run for First Strong character
5072					if ($chunkOTLdata['char_data'][$i]['uni'] == 8296) { // FSI
5073						$lvl = 0;
5074						$nc2 = $nc;
5075						$i2 = $i;
5076						while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) {  // while not at end of last chunk
5077							$i2++;
5078							if ($i2 >= count($para[$nc2][18]['char_data'])) {
5079								$nc2++;
5080								$i2 = 0;
5081							}
5082							if ($lvl > 0) {
5083								continue;
5084							}
5085							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) {
5086								$lvl++;
5087								continue;
5088							}
5089							if ($para[$nc2][18]['char_data'][$i2]['uni'] == 8297) {
5090								$lvl--;
5091								if ($lvl < 0) {
5092									break;
5093								}
5094							}
5095							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) {
5096								$fsi = $para[$nc2][18]['char_data'][$i2]['bidi_class'];
5097								break;
5098							}
5099						}
5100						// if fsi not found, fsi is same as paragraph embedding level
5101						if (!$fsi && $fsi !== 0) {
5102							if ($pel == 1) {
5103								$fsi = Ucdn::BIDI_CLASS_R;
5104							} else {
5105								$fsi = Ucdn::BIDI_CLASS_L;
5106							}
5107						}
5108					}
5109
5110					if ($chunkOTLdata['char_data'][$i]['uni'] == 8294 || $fsi === Ucdn::BIDI_CLASS_L) { // LRI or FSI-L
5111						//  Compute the least even embedding level greater than the embedding level of the last entry on the directional status stack.
5112						$next_level = $cel + 2 - ($cel % 2);
5113					} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8295 || $fsi == Ucdn::BIDI_CLASS_R || $fsi == Ucdn::BIDI_CLASS_AL) { // RLI or FSI-R
5114						//  Compute the least odd embedding level greater than the embedding level of the last entry on the directional status stack.
5115						$next_level = $cel + ($cel % 2) + 1;
5116					}
5117
5118
5119					//  Increment the isolate count by one, and push an entry consisting of the new embedding level,
5120					//  neutral directional override status, and true directional isolate status onto the directional status stack.
5121					$remember[] = ['num' => $chunkOTLdata['char_data'][$i]['uni'], 'cel' => $cel, 'dos' => $dos, 'diid' => $diid];
5122					$cel = $next_level;
5123					$dos = -1;
5124					$diid = ++$dictr; // Set new direction isolate ID after incrementing direction isolate counter
5125
5126					$controlchars = true;
5127				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 8297) { // PDI
5128					// X6a. With each PDI, perform the following steps:
5129					//  Pop the last entry from the directional status stack and decrement the isolate count by one.
5130					while (count($remember)) {
5131						$last = count($remember) - 1;
5132						if (($remember[$last]['num'] == 8294) || ($remember[$last]['num'] == 8295) || ($remember[$last]['num'] == 8296)) {
5133							$match = array_pop($remember);
5134							$cel = $match['cel'];
5135							$dos = $match['dos'];
5136							$diid = $match['diid'];
5137							break;
5138						} // End/close any open embedding states not explicitly closed during the isolate
5139						elseif (($remember[$last]['num'] == 8235) || ($remember[$last]['num'] == 8234) || ($remember[$last]['num'] == 8238) ||
5140							($remember[$last]['num'] == 8237)) {
5141							$match = array_pop($remember);
5142						}
5143					}
5144					//  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.
5145					//  NB The level assigned to an isolate initiator is always the same as that assigned to the matching PDI.
5146					if ($dos != -1) {
5147						$chardir = $dos;
5148					} else {
5149						$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
5150					}
5151					$chunkOTLdata['char_data'][$i]['level'] = $cel;
5152					$chunkOTLdata['char_data'][$i]['type'] = $chardir;
5153					$chunkOTLdata['char_data'][$i]['diid'] = $diid;
5154					$controlchars = true;
5155				} elseif ($chunkOTLdata['char_data'][$i]['uni'] == 10) { // NEW LINE
5156					// Reset to start values
5157					$cel = $pel;
5158					$dos = -1;
5159					$remember = [];
5160				} else {
5161					// X6. For all types besides RLE, LRE, RLO, LRO, and PDF:
5162					//  a. Set the level of the current character to the current embedding level.
5163					//  b. When the directional override status is not neutral, reset the current character type to directional override status.
5164					if ($dos != -1) {
5165						$chardir = $dos;
5166					} else {
5167						$chardir = $chunkOTLdata['char_data'][$i]['bidi_class'];
5168						if ($chardir == Ucdn::BIDI_CLASS_R || $chardir == Ucdn::BIDI_CLASS_AL) {
5169							$strongrtl = true;
5170						}
5171					}
5172					$chunkOTLdata['char_data'][$i]['level'] = $cel;
5173					$chunkOTLdata['char_data'][$i]['type'] = $chardir;
5174					$chunkOTLdata['char_data'][$i]['diid'] = $diid;
5175				}
5176			}
5177			// X8. All explicit directional embeddings and overrides are completely terminated at the end of each paragraph.
5178			// Paragraph separators are not included in the embedding.
5179			// X9. Remove all RLE, LRE, RLO, LRO, and PDF codes.
5180			if ($controlchars) {
5181				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xaa");
5182				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xab");
5183				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xac");
5184				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xad");
5185				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x80\xae");
5186				preg_replace("/\x{202a}-\x{202e}/u", '', $para[$nc][0]);
5187			}
5188		}
5189
5190		// Remove any blank chunks made by removing directional codes
5191		$numchunks = count($para);
5192		for ($nc = ($numchunks - 1); $nc >= 0; $nc--) {
5193			if (count($para[$nc][18]['char_data']) == 0) {
5194				array_splice($para, $nc, 1);
5195			}
5196		}
5197		if ($dir != 'rtl' && !$strongrtl && !$controlchars) {
5198			return;
5199		}
5200
5201		$numchunks = count($para);
5202
5203		// 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:
5204		// 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.
5205		// 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.
5206		// If the higher level is odd, the sor or eor is R; otherwise, it is L.
5207
5208		for ($ir = 0; $ir <= $dictr; $ir++) {
5209			$prelevel = $pel;
5210			$postlevel = $pel;
5211			$firstchar = true;
5212			for ($nc = 0; $nc < $numchunks; $nc++) {
5213				$chardata = & $para[$nc][18]['char_data'];
5214				$numchars = count($chardata);
5215				for ($i = 0; $i < $numchars; ++$i) {
5216					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5217						continue;
5218					} // Ignore characters in a different isolate run
5219					$right = $postlevel;
5220					$nc2 = $nc;
5221					$i2 = $i;
5222					while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) {  // while not at end of last chunk
5223						$i2++;
5224						if ($i2 >= count($para[$nc2][18]['char_data'])) {
5225							$nc2++;
5226							$i2 = 0;
5227						}
5228
5229						if (isset($para[$nc2][18]['char_data'][$i2]['diid']) && $para[$nc2][18]['char_data'][$i2]['diid'] == $ir) {
5230							$right = $para[$nc2][18]['char_data'][$i2]['level'];
5231							break;
5232						}
5233					}
5234
5235					$level = $chardata[$i]['level'];
5236					if ($firstchar || $level != $prelevel) {
5237						$chardata[$i]['sor'] = max($prelevel, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
5238					}
5239					if (($nc == ($numchunks - 1) && $i == ($numchars - 1)) || $level != $right) {
5240						$chardata[$i]['eor'] = max($right, $level) % 2 ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
5241					}
5242					$prelevel = $level;
5243					$firstchar = false;
5244				}
5245			}
5246		}
5247
5248
5249		// 3.3.3 Resolving Weak Types
5250		// 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.
5251		// Nonspacing marks are now resolved based on the previous characters.
5252		// 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.
5253		for ($ir = 0; $ir <= $dictr; $ir++) {
5254			$prevtype = 0;
5255			for ($nc = 0; $nc < $numchunks; $nc++) {
5256				$chardata = & $para[$nc][18]['char_data'];
5257				$numchars = count($chardata);
5258				for ($i = 0; $i < $numchars; ++$i) {
5259					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5260						continue;
5261					} // Ignore characters in a different isolate run
5262					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_NSM) {
5263						if (isset($chardata[$i]['sor'])) {
5264							$chardata[$i]['type'] = $chardata[$i]['sor'];
5265						} else {
5266							$chardata[$i]['type'] = $prevtype;
5267						}
5268					}
5269					$prevtype = $chardata[$i]['type'];
5270				}
5271			}
5272		}
5273
5274		// 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.
5275		for ($ir = 0; $ir <= $dictr; $ir++) {
5276			$laststrongtype = -1;
5277			for ($nc = 0; $nc < $numchunks; $nc++) {
5278				$chardata = & $para[$nc][18]['char_data'];
5279				$numchars = count($chardata);
5280				for ($i = 0; $i < $numchars; ++$i) {
5281					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5282						continue;
5283					} // Ignore characters in a different isolate run
5284					if (isset($chardata[$i]['sor'])) {
5285						$laststrongtype = $chardata[$i]['sor'];
5286					}
5287					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN && $laststrongtype == Ucdn::BIDI_CLASS_AL) {
5288						$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
5289					}
5290					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_R || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL) {
5291						$laststrongtype = $chardata[$i]['type'];
5292					}
5293				}
5294			}
5295		}
5296
5297
5298		// W3. Change all ALs to R.
5299		for ($nc = 0; $nc < $numchunks; $nc++) {
5300			$chardata = & $para[$nc][18]['char_data'];
5301			$numchars = count($chardata);
5302			for ($i = 0; $i < $numchars; ++$i) {
5303				if (isset($chardata[$i]['type']) && $chardata[$i]['type'] == Ucdn::BIDI_CLASS_AL) {
5304					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_R;
5305				}
5306			}
5307		}
5308
5309
5310		// 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.
5311		for ($ir = 0; $ir <= $dictr; $ir++) {
5312			$prevtype = -1;
5313			$nexttype = -1;
5314			for ($nc = 0; $nc < $numchunks; $nc++) {
5315				$chardata = & $para[$nc][18]['char_data'];
5316				$numchars = count($chardata);
5317				for ($i = 0; $i < $numchars; ++$i) {
5318					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5319						continue;
5320					} // Ignore characters in a different isolate run
5321					// Get next type
5322					$nexttype = -1;
5323					$nc2 = $nc;
5324					$i2 = $i;
5325					while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) {  // while not at end of last chunk
5326						$i2++;
5327						if ($i2 >= count($para[$nc2][18]['char_data'])) {
5328							$nc2++;
5329							$i2 = 0;
5330						}
5331
5332						if (isset($para[$nc2][18]['char_data'][$i2]['diid']) && $para[$nc2][18]['char_data'][$i2]['diid'] == $ir) {
5333							$nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5334							break;
5335						}
5336					}
5337
5338					if (!isset($chardata[$i]['sor']) && !isset($chardata[$i]['eor'])) {
5339						if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ES && $prevtype == Ucdn::BIDI_CLASS_EN && $nexttype == Ucdn::BIDI_CLASS_EN) {
5340							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5341						} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $prevtype == Ucdn::BIDI_CLASS_EN && $nexttype == Ucdn::BIDI_CLASS_EN) {
5342							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5343						} elseif ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_CS && $prevtype == Ucdn::BIDI_CLASS_AN && $nexttype == Ucdn::BIDI_CLASS_AN) {
5344							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_AN;
5345						}
5346					}
5347					$prevtype = $chardata[$i]['type'];
5348				}
5349			}
5350		}
5351
5352		// W5. A sequence of European terminators adjacent to European numbers changes to all European numbers.
5353		for ($ir = 0; $ir <= $dictr; $ir++) {
5354			$prevtype = -1;
5355			$nexttype = -1;
5356			for ($nc = 0; $nc < $numchunks; $nc++) {
5357				$chardata = & $para[$nc][18]['char_data'];
5358				$numchars = count($chardata);
5359				for ($i = 0; $i < $numchars; ++$i) {
5360					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5361						continue;
5362					} // Ignore characters in a different isolate run
5363					if (isset($chardata[$i]['sor'])) {
5364						$prevtype = $chardata[$i]['sor'];
5365					}
5366
5367					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ET) {
5368						if ($prevtype == Ucdn::BIDI_CLASS_EN) {
5369							$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5370						} elseif (!isset($chardata[$i]['eor'])) {
5371							$nexttype = -1;
5372							$nc2 = $nc;
5373							$i2 = $i;
5374							while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) { // while not at end of last chunk
5375								$i2++;
5376								if ($i2 >= count($para[$nc2][18]['char_data'])) {
5377									$nc2++;
5378									$i2 = 0;
5379								}
5380								if ($para[$nc2][18]['char_data'][$i2]['diid'] != $ir) {
5381									continue;
5382								}
5383								$nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5384								if (isset($para[$nc2][18]['char_data'][$i2]['sor'])) {
5385									break;
5386								}
5387								if ($nexttype == Ucdn::BIDI_CLASS_EN) {
5388									$chardata[$i]['type'] = Ucdn::BIDI_CLASS_EN;
5389									break;
5390								} elseif ($nexttype != Ucdn::BIDI_CLASS_ET) {
5391									break;
5392								}
5393							}
5394						}
5395					}
5396					$prevtype = $chardata[$i]['type'];
5397				}
5398			}
5399		}
5400
5401		// W6. Otherwise, separators and terminators change to Other Neutral.
5402		for ($nc = 0; $nc < $numchunks; $nc++) {
5403			$chardata = & $para[$nc][18]['char_data'];
5404			$numchars = count($chardata);
5405			for ($i = 0; $i < $numchars; ++$i) {
5406				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))) {
5407					$chardata[$i]['type'] = Ucdn::BIDI_CLASS_ON;
5408				}
5409			}
5410		}
5411
5412		//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.
5413		for ($ir = 0; $ir <= $dictr; $ir++) {
5414			$laststrongtype = -1;
5415			for ($nc = 0; $nc < $numchunks; $nc++) {
5416				$chardata = & $para[$nc][18]['char_data'];
5417				$numchars = count($chardata);
5418				for ($i = 0; $i < $numchars; ++$i) {
5419					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5420						continue;
5421					} // Ignore characters in a different isolate run
5422					if (isset($chardata[$i]['sor'])) {
5423						$laststrongtype = $chardata[$i]['sor'];
5424					}
5425					if (isset($chardata[$i]['type']) && $chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN && $laststrongtype == Ucdn::BIDI_CLASS_L) {
5426						$chardata[$i]['type'] = Ucdn::BIDI_CLASS_L;
5427					}
5428					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)) {
5429						$laststrongtype = $chardata[$i]['type'];
5430					}
5431				}
5432			}
5433		}
5434
5435		// 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.
5436		for ($ir = 0; $ir <= $dictr; $ir++) {
5437			$laststrongtype = -1;
5438			for ($nc = 0; $nc < $numchunks; $nc++) {
5439				$chardata = & $para[$nc][18]['char_data'];
5440				$numchars = count($chardata);
5441				for ($i = 0; $i < $numchars; ++$i) {
5442					if (!isset($chardata[$i]['diid']) || $chardata[$i]['diid'] != $ir) {
5443						continue;
5444					} // Ignore characters in a different isolate run
5445					if (isset($chardata[$i]['sor'])) {
5446						$laststrongtype = $chardata[$i]['sor'];
5447					}
5448					if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS) {
5449						$left = -1;
5450						// LEFT
5451						if ($laststrongtype == Ucdn::BIDI_CLASS_R || $laststrongtype == Ucdn::BIDI_CLASS_EN || $laststrongtype == Ucdn::BIDI_CLASS_AN) {
5452							$left = Ucdn::BIDI_CLASS_R;
5453						} elseif ($laststrongtype == Ucdn::BIDI_CLASS_L) {
5454							$left = Ucdn::BIDI_CLASS_L;
5455						}
5456						// RIGHT
5457						$right = -1;
5458						// move to the right of any following neutrals OR hit a run boundary
5459
5460						if (isset($chardata[$i]['eor'])) {
5461							$right = $chardata[$i]['eor'];
5462						} else {
5463							$nexttype = -1;
5464							$nc2 = $nc;
5465							$i2 = $i;
5466							while (!($nc2 == ($numchunks - 1) && $i2 == ((count($para[$nc2][18]['char_data'])) - 1))) { // while not at end of last chunk
5467								$i2++;
5468								if ($i2 >= count($para[$nc2][18]['char_data'])) {
5469									$nc2++;
5470									$i2 = 0;
5471								}
5472								if (!isset($para[$nc2][18]['char_data'][$i2]['diid']) || $para[$nc2][18]['char_data'][$i2]['diid'] != $ir) {
5473									continue;
5474								}
5475								$nexttype = $para[$nc2][18]['char_data'][$i2]['type'];
5476								if ($nexttype == Ucdn::BIDI_CLASS_R || $nexttype == Ucdn::BIDI_CLASS_EN || $nexttype == Ucdn::BIDI_CLASS_AN) {
5477									$right = Ucdn::BIDI_CLASS_R;
5478									break;
5479								} elseif ($nexttype == Ucdn::BIDI_CLASS_L) {
5480									$right = Ucdn::BIDI_CLASS_L;
5481									break;
5482								} elseif (isset($para[$nc2][18]['char_data'][$i2]['eor'])) {
5483									$right = $para[$nc2][18]['char_data'][$i2]['eor'];
5484									break;
5485								}
5486							}
5487						}
5488
5489						if ($left > -1 && $left == $right) {
5490							$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
5491							$chardata[$i]['type'] = $left;
5492						}
5493					} 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) {
5494						$laststrongtype = $chardata[$i]['type'];
5495					}
5496				}
5497			}
5498		}
5499
5500		// N2. Any remaining neutrals take the embedding direction
5501		for ($nc = 0; $nc < $numchunks; $nc++) {
5502			$chardata = & $para[$nc][18]['char_data'];
5503			$numchars = count($chardata);
5504			for ($i = 0; $i < $numchars; ++$i) {
5505				if (isset($chardata[$i]['type']) && ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_ON || $chardata[$i]['type'] == Ucdn::BIDI_CLASS_WS)) {
5506					$chardata[$i]['orig_type'] = $chardata[$i]['type']; // Need to store the original 'WS' for reference in L1 below
5507					$chardata[$i]['type'] = ($chardata[$i]['level'] % 2) ? Ucdn::BIDI_CLASS_R : Ucdn::BIDI_CLASS_L;
5508				}
5509			}
5510		}
5511
5512		// 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.
5513		// I2. For all characters with an odd (right-to-left) embedding direction, those of type L, EN or AN go up one level.
5514		for ($nc = 0; $nc < $numchunks; $nc++) {
5515			$chardata = & $para[$nc][18]['char_data'];
5516			$numchars = count($chardata);
5517			for ($i = 0; $i < $numchars; ++$i) {
5518				if (isset($chardata[$i]['level'])) {
5519					$odd = $chardata[$i]['level'] % 2;
5520					if ($odd) {
5521						if (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_L) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
5522							$chardata[$i]['level'] += 1;
5523						}
5524					} else {
5525						if ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_R) {
5526							$chardata[$i]['level'] += 1;
5527						} elseif (($chardata[$i]['type'] == Ucdn::BIDI_CLASS_AN) || ($chardata[$i]['type'] == Ucdn::BIDI_CLASS_EN)) {
5528							$chardata[$i]['level'] += 2;
5529						}
5530					}
5531				}
5532			}
5533		}
5534
5535		// Remove Isolate formatters
5536		$numchunks = count($para);
5537		if ($controlchars) {
5538			for ($nc = 0; $nc < $numchunks; $nc++) {
5539				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa6");
5540				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa7");
5541				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa8");
5542				$this->removeChar($para[$nc][0], $para[$nc][18], "\xe2\x81\xa9");
5543				preg_replace("/\x{2066}-\x{2069}/u", '', $para[$nc][0]);
5544			}
5545			// Remove any blank chunks made by removing directional codes
5546			for ($nc = ($numchunks - 1); $nc >= 0; $nc--) {
5547				if (count($para[$nc][18]['char_data']) == 0) {
5548					array_splice($para, $nc, 1);
5549				}
5550			}
5551		}
5552	}
5553
5554	/**
5555	 * Reorder, once divided into lines
5556	 */
5557	public function bidiReorder(&$chunkorder, &$content, &$cOTLdata, $blockdir)
5558	{
5559
5560		$bidiData = [];
5561
5562		// First combine into one array (and get the highest level in use)
5563		$numchunks = count($content);
5564		$maxlevel = 0;
5565		for ($nc = 0; $nc < $numchunks; $nc++) {
5566			$numchars = count($cOTLdata[$nc]['char_data']);
5567			for ($i = 0; $i < $numchars; ++$i) {
5568				$carac = [];
5569				if (isset($cOTLdata[$nc]['GPOSinfo'][$i])) {
5570					$carac['GPOSinfo'] = $cOTLdata[$nc]['GPOSinfo'][$i];
5571				}
5572				$carac['uni'] = $cOTLdata[$nc]['char_data'][$i]['uni'];
5573				if (isset($cOTLdata[$nc]['char_data'][$i]['type'])) {
5574					$carac['type'] = $cOTLdata[$nc]['char_data'][$i]['type'];
5575				}
5576				if (isset($cOTLdata[$nc]['char_data'][$i]['level'])) {
5577					$carac['level'] = $cOTLdata[$nc]['char_data'][$i]['level'];
5578				}
5579				if (isset($cOTLdata[$nc]['char_data'][$i]['orig_type'])) {
5580					$carac['orig_type'] = $cOTLdata[$nc]['char_data'][$i]['orig_type'];
5581				}
5582				$carac['group'] = $cOTLdata[$nc]['group']{$i};
5583				$carac['chunkid'] = $chunkorder[$nc]; // gives font id and/or object ID
5584
5585				$maxlevel = max((isset($carac['level']) ? $carac['level'] : 0), $maxlevel);
5586				$bidiData[] = $carac;
5587			}
5588		}
5589		if ($maxlevel == 0) {
5590			return;
5591		}
5592
5593		$numchars = count($bidiData);
5594
5595		// L1. On each line, reset the embedding level of the following characters to the paragraph embedding level:
5596		//  1. Segment separators (Tab) 'S',
5597		//  2. Paragraph separators 'B',
5598		//  3. Any sequence of whitespace characters 'WS' preceding a segment separator or paragraph separator, and
5599		//  4. Any sequence of whitespace characters 'WS' at the end of the line.
5600		//  The types of characters used here are the original types, not those modified by the previous phase cf N1 and N2*******
5601		//  Because a Paragraph Separator breaks lines, there will be at most one per line, at the end of that line.
5602		// Set the initial paragraph embedding level
5603		if ($blockdir == 'rtl') {
5604			$pel = 1;
5605		} else {
5606			$pel = 0;
5607		}
5608
5609		for ($i = ($numchars - 1); $i > 0; $i--) {
5610			if ($bidiData[$i]['type'] == Ucdn::BIDI_CLASS_WS || (isset($bidiData[$i]['orig_type']) && $bidiData[$i]['orig_type'] == Ucdn::BIDI_CLASS_WS)) {
5611				$bidiData[$i]['level'] = $pel;
5612			} else {
5613				break;
5614			}
5615		}
5616
5617
5618		// 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.
5619		for ($j = $maxlevel; $j > 0; $j--) {
5620			$ordarray = [];
5621			$revarr = [];
5622			$onlevel = false;
5623			for ($i = 0; $i < $numchars; ++$i) {
5624				if ($bidiData[$i]['level'] >= $j) {
5625					$onlevel = true;
5626					// 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.
5627					if (isset(Ucdn::$mirror_pairs[$bidiData[$i]['uni']]) && $bidiData[$i]['type'] == Ucdn::BIDI_CLASS_R) {
5628						$bidiData[$i]['uni'] = Ucdn::$mirror_pairs[$bidiData[$i]['uni']];
5629					}
5630
5631					$revarr[] = $bidiData[$i];
5632				} else {
5633					if ($onlevel) {
5634						$revarr = array_reverse($revarr);
5635						$ordarray = array_merge($ordarray, $revarr);
5636						$revarr = [];
5637						$onlevel = false;
5638					}
5639					$ordarray[] = $bidiData[$i];
5640				}
5641			}
5642			if ($onlevel) {
5643				$revarr = array_reverse($revarr);
5644				$ordarray = array_merge($ordarray, $revarr);
5645			}
5646			$bidiData = $ordarray;
5647		}
5648
5649		$content = [];
5650		$cOTLdata = [];
5651		$chunkorder = [];
5652
5653
5654
5655		$nc = -1; // New chunk order ID
5656		$chunkid = -1;
5657
5658		foreach ($bidiData as $carac) {
5659			if ($carac['chunkid'] != $chunkid) {
5660				$nc++;
5661				$chunkorder[$nc] = $carac['chunkid'];
5662				$cctr = 0;
5663				$content[$nc] = '';
5664				$cOTLdata[$nc]['group'] = '';
5665			}
5666			if ($carac['uni'] != 0xFFFC) {   // Object replacement character (65532)
5667				$content[$nc] .= code2utf($carac['uni']);
5668				$cOTLdata[$nc]['group'] .= $carac['group'];
5669				if (!empty($carac['GPOSinfo'])) {
5670					if (isset($carac['GPOSinfo'])) {
5671						$cOTLdata[$nc]['GPOSinfo'][$cctr] = $carac['GPOSinfo'];
5672					}
5673					$cOTLdata[$nc]['GPOSinfo'][$cctr]['wDir'] = ($carac['level'] % 2) ? 'RTL' : 'LTR';
5674				}
5675			}
5676			$chunkid = $carac['chunkid'];
5677			$cctr++;
5678		}
5679	}
5680
5681	public function splitOTLdata(&$cOTLdata, $OTLcutoffpos, $OTLrestartpos = '')
5682	{
5683		if (!$OTLrestartpos) {
5684			$OTLrestartpos = $OTLcutoffpos;
5685		}
5686		$newOTLdata = ['GPOSinfo' => [], 'char_data' => []];
5687		$newOTLdata['group'] = substr($cOTLdata['group'], $OTLrestartpos);
5688		$cOTLdata['group'] = substr($cOTLdata['group'], 0, $OTLcutoffpos);
5689
5690		if (isset($cOTLdata['GPOSinfo']) && $cOTLdata['GPOSinfo']) {
5691			foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5692				if ($k >= $OTLrestartpos) {
5693					$newOTLdata['GPOSinfo'][($k - $OTLrestartpos)] = $val;
5694				}
5695				if ($k >= $OTLcutoffpos) {
5696					unset($cOTLdata['GPOSinfo'][$k]);
5697					//$cOTLdata['GPOSinfo'][$k] = array();
5698				}
5699			}
5700		}
5701		if (isset($cOTLdata['char_data'])) {
5702			$newOTLdata['char_data'] = array_slice($cOTLdata['char_data'], $OTLrestartpos);
5703			array_splice($cOTLdata['char_data'], $OTLcutoffpos);
5704		}
5705
5706		// Not necessary - easier to debug
5707		if (isset($cOTLdata['GPOSinfo'])) {
5708			ksort($cOTLdata['GPOSinfo']);
5709		}
5710		if (isset($newOTLdata['GPOSinfo'])) {
5711			ksort($newOTLdata['GPOSinfo']);
5712		}
5713
5714		return $newOTLdata;
5715	}
5716
5717	public function sliceOTLdata($OTLdata, $pos, $len)
5718	{
5719		$newOTLdata = ['GPOSinfo' => [], 'char_data' => []];
5720		$newOTLdata['group'] = substr($OTLdata['group'], $pos, $len);
5721
5722		if ($OTLdata['GPOSinfo']) {
5723			foreach ($OTLdata['GPOSinfo'] as $k => $val) {
5724				if ($k >= $pos && $k < ($pos + $len)) {
5725					$newOTLdata['GPOSinfo'][($k - $pos)] = $val;
5726				}
5727			}
5728		}
5729
5730		if (isset($OTLdata['char_data'])) {
5731			$newOTLdata['char_data'] = array_slice($OTLdata['char_data'], $pos, $len);
5732		}
5733
5734		// Not necessary - easier to debug
5735		if ($newOTLdata['GPOSinfo']) {
5736			ksort($newOTLdata['GPOSinfo']);
5737		}
5738
5739		return $newOTLdata;
5740	}
5741
5742	/**
5743	 * Remove one or more occurrences of $char (single character) from $txt and adjust OTLdata
5744	 */
5745	public function removeChar(&$txt, &$cOTLdata, $char)
5746	{
5747		while (mb_strpos($txt, $char, 0, $this->mpdf->mb_enc) !== false) {
5748			$pos = mb_strpos($txt, $char, 0, $this->mpdf->mb_enc);
5749			$newGPOSinfo = [];
5750			$cOTLdata['group'] = substr_replace($cOTLdata['group'], '', $pos, 1);
5751			if ($cOTLdata['GPOSinfo']) {
5752				foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5753					if ($k > $pos) {
5754						$newGPOSinfo[($k - 1)] = $val;
5755					} elseif ($k != $pos) {
5756						$newGPOSinfo[$k] = $val;
5757					}
5758				}
5759				$cOTLdata['GPOSinfo'] = $newGPOSinfo;
5760			}
5761			if (isset($cOTLdata['char_data'])) {
5762				array_splice($cOTLdata['char_data'], $pos, 1);
5763			}
5764
5765			$txt = preg_replace("/" . $char . "/", '', $txt, 1);
5766		}
5767	}
5768
5769	/**
5770	 * Remove one or more occurrences of $char (single character) from $txt and adjust OTLdata
5771	 */
5772	public function replaceSpace(&$txt, &$cOTLdata)
5773	{
5774		$char = chr(194) . chr(160); // NBSP
5775		while (mb_strpos($txt, $char, 0, $this->mpdf->mb_enc) !== false) {
5776			$pos = mb_strpos($txt, $char, 0, $this->mpdf->mb_enc);
5777			if ($cOTLdata['char_data'][$pos]['uni'] == 160) {
5778				$cOTLdata['char_data'][$pos]['uni'] = 32;
5779			}
5780			$txt = preg_replace("/" . $char . "/", ' ', $txt, 1);
5781		}
5782	}
5783
5784	public function trimOTLdata(&$cOTLdata, $Left = true, $Right = true)
5785	{
5786		$len = count($cOTLdata['char_data']);
5787		$nLeft = 0;
5788		$nRight = 0;
5789		for ($i = 0; $i < $len; $i++) {
5790			if ($cOTLdata['char_data'][$i]['uni'] == 32 || $cOTLdata['char_data'][$i]['uni'] == 12288) {
5791				$nLeft++;
5792			} // 12288 = 0x3000 = CJK space
5793			else {
5794				break;
5795			}
5796		}
5797		for ($i = ($len - 1); $i >= 0; $i--) {
5798			if ($cOTLdata['char_data'][$i]['uni'] == 32 || $cOTLdata['char_data'][$i]['uni'] == 12288) {
5799				$nRight++;
5800			} // 12288 = 0x3000 = CJK space
5801			else {
5802				break;
5803			}
5804		}
5805
5806		// Trim Right
5807		if ($Right && $nRight) {
5808			$cOTLdata['group'] = substr($cOTLdata['group'], 0, strlen($cOTLdata['group']) - $nRight);
5809			if ($cOTLdata['GPOSinfo']) {
5810				foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5811					if ($k >= $len - $nRight) {
5812						unset($cOTLdata['GPOSinfo'][$k]);
5813					}
5814				}
5815			}
5816			if (isset($cOTLdata['char_data'])) {
5817				for ($i = 0; $i < $nRight; $i++) {
5818					array_pop($cOTLdata['char_data']);
5819				}
5820			}
5821		}
5822		// Trim Left
5823		if ($Left && $nLeft) {
5824			$cOTLdata['group'] = substr($cOTLdata['group'], $nLeft);
5825			if ($cOTLdata['GPOSinfo']) {
5826				$newPOSinfo = [];
5827				foreach ($cOTLdata['GPOSinfo'] as $k => $val) {
5828					if ($k >= $nLeft) {
5829						$newPOSinfo[$k - $nLeft] = $cOTLdata['GPOSinfo'][$k];
5830					}
5831				}
5832				$cOTLdata['GPOSinfo'] = $newPOSinfo;
5833			}
5834			if (isset($cOTLdata['char_data'])) {
5835				for ($i = 0; $i < $nLeft; $i++) {
5836					array_shift($cOTLdata['char_data']);
5837				}
5838			}
5839		}
5840	}
5841
5842	////////////////////////////////////////////////////////////////
5843	//////////         GENERAL OTL FUNCTIONS       /////////////////
5844	////////////////////////////////////////////////////////////////
5845
5846	private function glyphToChar($gid)
5847	{
5848		return (ord($this->glyphIDtoUni[$gid * 3]) << 16) + (ord($this->glyphIDtoUni[$gid * 3 + 1]) << 8) + ord($this->glyphIDtoUni[$gid * 3 + 2]);
5849	}
5850
5851	private function unicode_hex($unicode_dec)
5852	{
5853		return (str_pad(strtoupper(dechex($unicode_dec)), 5, '0', STR_PAD_LEFT));
5854	}
5855
5856	private function seek($pos)
5857	{
5858		$this->_pos = $pos;
5859	}
5860
5861	private function skip($delta)
5862	{
5863		$this->_pos += $delta;
5864	}
5865
5866	private function read_short()
5867	{
5868		$a = (ord($this->ttfOTLdata[$this->_pos]) << 8) + ord($this->ttfOTLdata[$this->_pos + 1]);
5869		if ($a & (1 << 15)) {
5870			$a = ($a - (1 << 16));
5871		}
5872		$this->_pos += 2;
5873		return $a;
5874	}
5875
5876	private function read_ushort()
5877	{
5878		$a = (ord($this->ttfOTLdata[$this->_pos]) << 8) + ord($this->ttfOTLdata[$this->_pos + 1]);
5879		$this->_pos += 2;
5880		return $a;
5881	}
5882
5883	private function _getCoverageGID()
5884	{
5885		// Called from Lookup Type 1, Format 1 - returns glyphIDs rather than hexstrings
5886		// Need to do this separately to cache separately
5887		// Otherwise the same as fn below _getCoverage
5888		$offset = $this->_pos;
5889		if (isset($this->LuDataCache[$this->fontkey]['GID'][$offset])) {
5890			$g = $this->LuDataCache[$this->fontkey]['GID'][$offset];
5891		} else {
5892			$g = [];
5893			$CoverageFormat = $this->read_ushort();
5894			if ($CoverageFormat == 1) {
5895				$CoverageGlyphCount = $this->read_ushort();
5896				for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) {
5897					$glyphID = $this->read_ushort();
5898					$g[] = $glyphID;
5899				}
5900			}
5901			if ($CoverageFormat == 2) {
5902				$RangeCount = $this->read_ushort();
5903				for ($r = 0; $r < $RangeCount; $r++) {
5904					$start = $this->read_ushort();
5905					$end = $this->read_ushort();
5906					$StartCoverageIndex = $this->read_ushort(); // n/a
5907					for ($glyphID = $start; $glyphID <= $end; $glyphID++) {
5908						$g[] = $glyphID;
5909					}
5910				}
5911			}
5912			$this->LuDataCache[$this->fontkey]['GID'][$offset] = $g;
5913		}
5914		return $g;
5915	}
5916
5917	private function _getCoverage()
5918	{
5919		$offset = $this->_pos;
5920		if (isset($this->LuDataCache[$this->fontkey][$offset])) {
5921			$g = $this->LuDataCache[$this->fontkey][$offset];
5922		} else {
5923			$g = [];
5924			$CoverageFormat = $this->read_ushort();
5925			if ($CoverageFormat == 1) {
5926				$CoverageGlyphCount = $this->read_ushort();
5927				for ($gid = 0; $gid < $CoverageGlyphCount; $gid++) {
5928					$glyphID = $this->read_ushort();
5929					$g[] = $this->unicode_hex($this->glyphToChar($glyphID));
5930				}
5931			}
5932			if ($CoverageFormat == 2) {
5933				$RangeCount = $this->read_ushort();
5934				for ($r = 0; $r < $RangeCount; $r++) {
5935					$start = $this->read_ushort();
5936					$end = $this->read_ushort();
5937					$StartCoverageIndex = $this->read_ushort(); // n/a
5938					for ($glyphID = $start; $glyphID <= $end; $glyphID++) {
5939						$g[] = $this->unicode_hex($this->glyphToChar($glyphID));
5940					}
5941				}
5942			}
5943			$this->LuDataCache[$this->fontkey][$offset] = $g;
5944		}
5945		return $g;
5946	}
5947
5948	private function _getClasses($offset)
5949	{
5950		if (isset($this->LuDataCache[$this->fontkey][$offset])) {
5951			$GlyphByClass = $this->LuDataCache[$this->fontkey][$offset];
5952		} else {
5953			$this->seek($offset);
5954			$ClassFormat = $this->read_ushort();
5955			$GlyphByClass = [];
5956			if ($ClassFormat == 1) {
5957				$StartGlyph = $this->read_ushort();
5958				$GlyphCount = $this->read_ushort();
5959				for ($i = 0; $i < $GlyphCount; $i++) {
5960					$startGlyphID = $StartGlyph + $i;
5961					$endGlyphID = $StartGlyph + $i;
5962					$class = $this->read_ushort();
5963					// Note: Font FreeSerif , tag "blws"
5964					// $BacktrackClasses[0] is defined ? a mistake in the font ???
5965					// Let's ignore for now
5966					if ($class > 0) {
5967						for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
5968							if ($this->glyphToChar($g)) {
5969								$GlyphByClass[$class][$this->glyphToChar($g)] = 1;
5970							}
5971						}
5972					}
5973				}
5974			} elseif ($ClassFormat == 2) {
5975				$tableCount = $this->read_ushort();
5976				for ($i = 0; $i < $tableCount; $i++) {
5977					$startGlyphID = $this->read_ushort();
5978					$endGlyphID = $this->read_ushort();
5979					$class = $this->read_ushort();
5980					// Note: Font FreeSerif , tag "blws"
5981					// $BacktrackClasses[0] is defined ? a mistake in the font ???
5982					// Let's ignore for now
5983					if ($class > 0) {
5984						for ($g = $startGlyphID; $g <= $endGlyphID; $g++) {
5985							if ($this->glyphToChar($g)) {
5986								$GlyphByClass[$class][$this->glyphToChar($g)] = 1;
5987							}
5988						}
5989					}
5990				}
5991			}
5992			$this->LuDataCache[$this->fontkey][$offset] = $GlyphByClass;
5993		}
5994		return $GlyphByClass;
5995	}
5996
5997	private function _getOTLscriptTag($ScriptLang, $scripttag, $scriptblock, $shaper, $useOTL, $mode)
5998	{
5999		// ScriptLang is the array of available script/lang tags supported by the font
6000		// $scriptblock is the (number/code) for the script of the actual text string based on Unicode properties (Ucdn::$uni_scriptblock)
6001		// $scripttag is the default tag derived from $scriptblock
6002		/*
6003		  http://www.microsoft.com/typography/otspec/ttoreg.htm
6004		  http://www.microsoft.com/typography/otspec/scripttags.htm
6005
6006		  Values for useOTL
6007
6008		  Bit   dn  hn  Value
6009		  1 1   0x0001  GSUB/GPOS - Latin scripts
6010		  2 2   0x0002  GSUB/GPOS - Cyrillic scripts
6011		  3 4   0x0004  GSUB/GPOS - Greek scripts
6012		  4 8   0x0008  GSUB/GPOS - CJK scripts (excluding Hangul-Jamo)
6013		  5 16  0x0010  (Reserved)
6014		  6 32  0x0020  (Reserved)
6015		  7 64  0x0040  (Reserved)
6016		  8 128 0x0080  GSUB/GPOS - All other scripts (including all RTL scripts, complex scripts with shapers etc)
6017
6018		  NB If change for RTL - cf. function magic_reverse_dir in mpdf.php to update
6019
6020		 */
6021
6022
6023		if ($scriptblock == Ucdn::SCRIPT_LATIN) {
6024			if (!($useOTL & 0x01)) {
6025				return ['', false];
6026			}
6027		} elseif ($scriptblock == Ucdn::SCRIPT_CYRILLIC) {
6028			if (!($useOTL & 0x02)) {
6029				return ['', false];
6030			}
6031		} elseif ($scriptblock == Ucdn::SCRIPT_GREEK) {
6032			if (!($useOTL & 0x04)) {
6033				return ['', false];
6034			}
6035		} elseif ($scriptblock >= Ucdn::SCRIPT_HIRAGANA && $scriptblock <= Ucdn::SCRIPT_YI) {
6036			if (!($useOTL & 0x08)) {
6037				return ['', false];
6038			}
6039		} else {
6040			if (!($useOTL & 0x80)) {
6041				return ['', false];
6042			}
6043		}
6044
6045		//  If availabletags includes scripttag - choose
6046		if (isset($ScriptLang[$scripttag])) {
6047			return [$scripttag, false];
6048		}
6049
6050		//  If INDIC (or Myanmar) and available tag not includes new version, check if includes old version & choose old version
6051		if ($shaper) {
6052			switch ($scripttag) {
6053				case 'bng2':
6054					if (isset($ScriptLang['beng'])) {
6055						return ['beng', true];
6056					}
6057					// fallthrough
6058				case 'dev2':
6059					if (isset($ScriptLang['deva'])) {
6060						return ['deva', true];
6061					}
6062					// fallthrough
6063				case 'gjr2':
6064					if (isset($ScriptLang['gujr'])) {
6065						return ['gujr', true];
6066					}
6067					// fallthrough
6068				case 'gur2':
6069					if (isset($ScriptLang['guru'])) {
6070						return ['guru', true];
6071					}
6072					// fallthrough
6073				case 'knd2':
6074					if (isset($ScriptLang['knda'])) {
6075						return ['knda', true];
6076					}
6077					// fallthrough
6078				case 'mlm2':
6079					if (isset($ScriptLang['mlym'])) {
6080						return ['mlym', true];
6081					}
6082					// fallthrough
6083				case 'ory2':
6084					if (isset($ScriptLang['orya'])) {
6085						return ['orya', true];
6086					}
6087					// fallthrough
6088				case 'tml2':
6089					if (isset($ScriptLang['taml'])) {
6090						return ['taml', true];
6091					}
6092					// fallthrough
6093				case 'tel2':
6094					if (isset($ScriptLang['telu'])) {
6095						return ['telu', true];
6096					}
6097					// fallthrough
6098				case 'mym2':
6099					if (isset($ScriptLang['mymr'])) {
6100						return ['mymr', true];
6101					}
6102			}
6103		}
6104
6105		//  choose DFLT if present
6106		if (isset($ScriptLang['DFLT'])) {
6107			return ['DFLT', false];
6108		}
6109		//  else choose dflt if present
6110		if (isset($ScriptLang['dflt'])) {
6111			return ['dflt', false];
6112		}
6113		//  else return no scriptTag
6114		if (isset($ScriptLang['latn'])) {
6115			return ['latn', false];
6116		}
6117		//  else return no scriptTag
6118		return ['', false];
6119	}
6120
6121	// LangSys tags
6122	private function _getOTLLangTag($ietf, $available)
6123	{
6124		// http://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
6125		// http://www.microsoft.com/typography/otspec/languagetags.htm
6126		// IETF tag = e.g. en-US, und-Arab, sr-Cyrl cf. class LangToFont
6127		if ($available == '') {
6128			return '';
6129		}
6130		$tags = preg_split('/-/', $ietf);
6131		$lang = '';
6132		$country = '';
6133		$script = '';
6134		$lang = strtolower($tags[0]);
6135		if (isset($tags[1]) && $tags[1]) {
6136			if (strlen($tags[1]) == 2) {
6137				$country = strtolower($tags[1]);
6138			}
6139		}
6140		if (isset($tags[2]) && $tags[2]) {
6141			$country = strtolower($tags[2]);
6142		}
6143
6144		if ($lang != '' && isset(Ucdn::$ot_languages[$lang])) {
6145			$langsys = Ucdn::$ot_languages[$lang];
6146		} elseif ($lang != '' && $country != '' && isset(Ucdn::$ot_languages[$lang . '' . $country])) {
6147			$langsys = Ucdn::$ot_languages[$lang . '' . $country];
6148		} else {
6149			$langsys = "DFLT";
6150		}
6151		if (strpos($available, $langsys) === false) {
6152			if (strpos($available, "DFLT") !== false) {
6153				return "DFLT";
6154			} else {
6155				return '';
6156			}
6157		}
6158		return $langsys;
6159	}
6160
6161	private function _dumpproc($GPOSSUB, $lookupID, $subtable, $Type, $Format, $ptr, $currGlyph, $level)
6162	{
6163		echo '<div style="padding-left: ' . ($level * 2) . 'em;">';
6164		echo $GPOSSUB . ' LookupID #' . $lookupID . ' Subtable#' . $subtable . ' Type: ' . $Type . ' Format: ' . $Format . '<br />';
6165		echo '<div style="font-family:monospace">';
6166		echo 'Glyph position: ' . $ptr . ' Current Glyph: ' . $currGlyph . '<br />';
6167
6168		for ($i = 0; $i < count($this->OTLdata); $i++) {
6169			if ($i == $ptr) {
6170				echo '<b>';
6171			}
6172			echo $this->OTLdata[$i]['hex'] . ' ';
6173			if ($i == $ptr) {
6174				echo '</b>';
6175			}
6176		}
6177		echo '<br />';
6178
6179		for ($i = 0; $i < count($this->OTLdata); $i++) {
6180			if ($i == $ptr) {
6181				echo '<b>';
6182			}
6183			echo str_pad($this->OTLdata[$i]['uni'], 5) . ' ';
6184			if ($i == $ptr) {
6185				echo '</b>';
6186			}
6187		}
6188		echo '<br />';
6189
6190		if ($GPOSSUB == 'GPOS') {
6191			for ($i = 0; $i < count($this->OTLdata); $i++) {
6192				if (!empty($this->OTLdata[$i]['GPOSinfo'])) {
6193					echo $this->OTLdata[$i]['hex'] . ' &#x' . $this->OTLdata[$i]['hex'] . '; ';
6194					print_r($this->OTLdata[$i]['GPOSinfo']);
6195					echo ' ';
6196				}
6197			}
6198		}
6199
6200		echo '</div>';
6201		echo '</div>';
6202	}
6203}
6204