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