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