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