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 = $