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 خْرَىٰ 502 فَتُذَكِّر 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 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 // င်္က္ကျြွေိ 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) يَنتُم 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">င်္က္ကျြွေိ</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">ру́сский</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: ನ್ನು and ಕ್ರೌ 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: إِلَىٰٓ 3724 // Test: arabictypesetting: بَّيْنَكُمْ 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 น้ำ 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