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