font.py (20721B)
1 #Embedded file name: C:\BuildAgent\work\20d73f1b24eed06e\eve\release\V24.01\packages\carbonui\services\font.py 2 import blue 3 import telemetry 4 import types 5 import trinity 6 import carbonui.const as uiconst 7 import carbonui.languageConst as languageConst 8 from carbonui import fontconst 9 from carbonui.text.settings import check_convert_font_size 10 from carbonui.util.stringManip import GetAsUnicode 11 from carbonui.util.bunch import Bunch 12 from carbon.common.script.util.commonutils import StripTags 13 from carbonui.uicore import uicore 14 from eve.client.script.ui.control.eveLabel import Label 15 16 class GlyphStringBoundingBox(object): 17 18 def __init__(self): 19 self.xMin = 0 20 self.xMax = 0 21 self.yMin = 0 22 self.yMax = 0 23 24 def Width(self): 25 return self.xMax - self.xMin 26 27 def Height(self): 28 return self.yMax - self.yMin 29 30 31 class Tr2GlyphString(list): 32 __guid__ = 'font.Tr2GlyphString' 33 34 def __init__(self, other = None): 35 if other: 36 list.__init__(self, other) 37 self.Reset() 38 39 def __iadd__(self, other): 40 list.extend(self, other) 41 self._textDirty = True 42 return self 43 44 def Reset(self): 45 self.baseLine = 0 46 self.baseHeight = 0 47 self.last = (None, 0) 48 self.asc = 0 49 self.des = 0 50 self.width = 0 51 self.strong = 0 52 self.shadow = None 53 self.fontsize = 0 54 self.text = u'' 55 self._textDirty = True 56 while self: 57 list.pop(self) 58 59 @telemetry.ZONE_METHOD 60 def Remove(self, startIdx, endIdx): 61 self._textDirty = True 62 for i in xrange(startIdx, endIdx): 63 self.pop(startIdx) 64 65 def FlushFromIndex(self, idx): 66 self._textDirty = True 67 while len(self) > idx: 68 list.pop(self, -1) 69 70 def FlushToIndex(self, idx): 71 self._textDirty = True 72 for i in xrange(idx): 73 if len(self): 74 list.pop(self, 0) 75 76 @telemetry.ZONE_METHOD 77 def Insert(self, params, text, idx, align = 0): 78 if not text: 79 return None 80 self._textDirty = True 81 color = params.color or -1 82 if type(color) != types.IntType: 83 tricol = trinity.TriColor(*color) 84 color = tricol.AsInt() 85 uicore.font.ResolveFontFamily(params) 86 dpi_wordspace = int(params.wordspace * uicore.desktop.dpiScaling + 0.5) 87 dpi_fontsize = int(check_convert_font_size(params.fontsize) * uicore.desktop.dpiScaling + 0.5) 88 dpi_letterspace = int(params.letterspace * uicore.desktop.dpiScaling + 0.5) 89 self.fontsize = dpi_fontsize 90 lastFontIndex, lastGlyphIndex = (None, None) 91 for char in text: 92 if char == u'\xa0': 93 rchar = u' ' 94 else: 95 rchar = char 96 fontIndex, glyphIndex = trinity.fontMan.LookupGlyphIndex(params.font, ord(rchar)) 97 if fontIndex != lastFontIndex: 98 self.GetMetrics(fontIndex) 99 self.baseLine = max(self.baseLine, self.asc) 100 self.baseHeight = max(self.baseHeight, self.asc - self.des) 101 kern = 0 102 else: 103 kern = trinity.fontMan.LookupKerningXP(fontIndex, glyphIndex, lastGlyphIndex) 104 lastFontIndex, lastGlyphIndex = fontIndex, glyphIndex 105 sbit = trinity.fontMan.LookupSBit(fontIndex, dpi_fontsize, dpi_fontsize, glyphIndex) 106 advance = sbit.xadvance + dpi_letterspace + kern 107 if rchar == u' ': 108 advance += dpi_wordspace 109 self.insert(idx, (advance, 110 align, 111 color, 112 sbit, 113 char, 114 self.asc, 115 self.des, 116 params.Copy())) 117 idx += 1 118 self.width += advance 119 120 @telemetry.ZONE_METHOD 121 def GetText(self): 122 if self._textDirty: 123 self.text = ''.join([ charData[4] for charData in self ]) 124 self._textDirty = False 125 return self.text 126 127 @telemetry.ZONE_METHOD 128 def Append(self, params, text, align = 0): 129 idx = len(self) 130 self.Insert(params, text, idx, align) 131 132 @telemetry.ZONE_METHOD 133 def Reload(self): 134 data = [ (params, char) for advance, align, color, sbit, char, asc, des, params in self ] 135 del self[:] 136 for params, char in data: 137 self.Append(params, char) 138 139 @telemetry.ZONE_METHOD 140 def DrawToBuf(self, buf, bx0, by0, checkUnderline = 1): 141 x = 0 142 for t in self: 143 advance = t[0] 144 sbit = t[3] 145 params = t[7] 146 color = params.color or -1 147 if type(color) != types.IntType: 148 tricol = trinity.TriColor(*color) 149 color = tricol.AsInt() 150 hasUnderline = bool(params.underline) 151 if hasUnderline: 152 extraSpace = advance - sbit.xadvance 153 sbit.ToBufferWithUnderline(buf.data, buf.width, buf.height, buf.pitch, x + bx0, by0, color, extraSpace) 154 else: 155 sbit.ToBuffer(buf.data, buf.width, buf.height, buf.pitch, x + bx0, by0, color) 156 x += t[0] 157 158 @telemetry.ZONE_METHOD 159 def GetMetrics(self, face_id): 160 self.asc, self.des = trinity.fontMan.LookupMetrics(face_id, self.fontsize, self.fontsize) 161 162 @telemetry.ZONE_METHOD 163 def AddSpace(self, params, width = 0.0): 164 uicore.font.ResolveFontFamily(params) 165 fgi = trinity.fontMan.LookupGlyphIndex(params.font, ord(' ')) 166 sbit = trinity.fontMan.LookupSBit(fgi[0], self.fontsize, self.fontsize, fgi[1]) 167 self.append((width, 168 0, 169 -1, 170 sbit, 171 ' ', 172 0, 173 0, 174 params)) 175 self.width = max(self.width, self.width + width) 176 177 @telemetry.ZONE_METHOD 178 def GetBBox(self): 179 baseLine = baseHeight = 0 180 bbox = GlyphStringBoundingBox() 181 x = 0 182 for t in self: 183 sbit = t[3] 184 bbox.xMin = min(sbit.x + sbit.xOffset + x, bbox.xMin) 185 bbox.xMax = max(sbit.x + sbit.xOffset + x + sbit.width, bbox.xMax) 186 bbox.yMin = min(sbit.y + sbit.yOffset, bbox.yMin) 187 bbox.yMax = max(sbit.y + sbit.yOffset + sbit.height, bbox.yMax) 188 baseLine = max(baseLine, t[5]) 189 baseHeight = max(baseHeight, t[5] - t[6]) 190 x += t[0] 191 192 self.baseLine = baseLine 193 self.baseHeight = baseHeight 194 self.width = x 195 return bbox 196 197 def GetWidth(self): 198 self.GetBBox() 199 return self.width 200 201 202 import re 203 tag = re.compile('<color=0x.*?>|</color>|<right>|<center>|<left>') 204 205 class FontHandler(object): 206 defaultLabelClass = Label 207 __notifyevents__ = ['OnUIRefresh'] 208 209 def __init__(self, *args, **kwds): 210 self.Prime() 211 sm.RegisterNotify(self) 212 213 def Prime(self): 214 self.measureCache = {} 215 self.textMeasureCache = {} 216 self.textLanguageIDCache = {} 217 regexes = [] 218 for languageID, windowsLanguageID, REGEX, isExclusive in (('JA', 219 languageConst.LANG_JAPANESE, 220 (uiconst.JA_CHARACTERS,), 221 True), 222 ('ZH', 223 languageConst.LANG_CHINESE, 224 (uiconst.REGEX_RANGE_CJK_IDEOGRAPHS,), 225 False), 226 ('JA', 227 languageConst.LANG_JAPANESE, 228 (uiconst.REGEX_RANGE_HIRAGANA, 229 uiconst.REGEX_RANGE_KATAKANA, 230 uiconst.REGEX_RANGE_KATAKANA_PHONETIC_EXTENSIONS, 231 uiconst.REGEX_RANGE_HALF_WIDTH_KATAKANA), 232 False), 233 ('KO', 234 languageConst.LANG_KOREAN, 235 (uiconst.REGEX_RANGE_HANGUL,), 236 False)): 237 compiledRegex = re.compile(u'([' + u']+ *|['.join(REGEX) + u']+ *)', re.UNICODE) 238 regexes.append((languageID, 239 windowsLanguageID, 240 compiledRegex, 241 isExclusive)) 242 243 self.regexes = regexes 244 245 def OnUIRefresh(self): 246 self.Prime() 247 248 def DeTag(self, s): 249 return tag.sub('', s) 250 251 @telemetry.ZONE_METHOD 252 def GetTextHeight(self, strng, width = 0, font = None, fontsize = None, lineSpacing = 0.0, letterspace = 0, shadow = [(1, -1, -1090519040)], uppercase = 0, specialIndent = 0, getTextObj = 0, tabs = [], fontPath = None, fontStyle = None, fontFamily = None, linespace = None, maxLines = None, bold = False, **kwds): 253 if strng is None or strng == '': 254 return 0 255 fontsize = check_convert_font_size(fontsize or fontconst.DEFAULT_FONTSIZE) 256 letterspace = letterspace or fontconst.DEFAULT_LETTERSPACE 257 uppercase = uppercase or fontconst.DEFAULT_UPPERCASE 258 if maxLines == 1: 259 width = 0 260 strng = u'\xd3gd' 261 else: 262 strng = GetAsUnicode(strng) 263 cacheKey = (strng, 264 width, 265 font, 266 fontsize, 267 lineSpacing, 268 letterspace, 269 maxLines, 270 uppercase, 271 specialIndent, 272 getTextObj, 273 1.0, 274 bold) 275 cache = self.GetCache(cacheKey) 276 if cache: 277 return cache 278 if self.defaultLabelClass is None: 279 from carbonui.control.label import LabelCore 280 self.defaultLabelClass = LabelCore 281 t = self.defaultLabelClass(text=strng, parent=None, align=uiconst.TOPLEFT, width=width, tabs=tabs, fontsize=fontsize, lineSpacing=lineSpacing, letterspace=letterspace, maxLines=maxLines, uppercase=uppercase, specialIndent=specialIndent, fontPath=fontPath, fontStyle=fontStyle, fontFamily=fontFamily, shadow=shadow, measuringText=not getTextObj, color=None, bold=bold) 282 if getTextObj: 283 retval = t 284 else: 285 retval = t.textheight 286 self.SetCache(cacheKey, retval) 287 return retval 288 289 @telemetry.ZONE_METHOD 290 def GetTextWidth(self, strng, fontsize = 12, letterspace = 0, uppercase = 0, font = None, fontPath = None, fontStyle = None, fontFamily = None, bold = False): 291 if not strng: 292 return 0 293 font = font 294 fontsize = check_convert_font_size(fontsize or fontconst.DEFAULT_FONTSIZE) 295 letterspace = letterspace or fontconst.DEFAULT_LETTERSPACE 296 uppercase = uppercase or fontconst.DEFAULT_UPPERCASE 297 cacheKey = (strng, 298 fontsize, 299 letterspace, 300 uppercase, 301 font, 302 1.0, 303 bold) 304 if cacheKey in self.textMeasureCache: 305 return self.textMeasureCache[cacheKey] 306 if '<br>' in strng: 307 val = max([ self.GetTextWidth(line, fontsize, letterspace, uppercase, font, fontPath, fontStyle, fontFamily, bold) for line in strng.split('<br>') ]) 308 else: 309 if self.defaultLabelClass is None: 310 from carbonui.control.label import LabelCore 311 self.defaultLabelClass = LabelCore 312 textmeasurer = self.defaultLabelClass(text=StripTags(strng, ignoredTags=['b']), parent=None, align=uiconst.TOPLEFT, fontsize=fontsize, letterspace=letterspace, uppercase=uppercase, measuringText=True, fontPath=fontPath, fontStyle=fontStyle, fontFamily=fontFamily, color=None, bold=bold) 313 val = textmeasurer.textwidth 314 self.textMeasureCache[cacheKey] = val 315 return val 316 317 @telemetry.ZONE_METHOD 318 def MeasureTabstops(self, stringsData, margin = None, minwidth = 32): 319 if margin is None: 320 margin = uiconst.LABELTABMARGIN 321 tabstops = [] 322 tabCount = 0 323 for stringData in stringsData: 324 if len(stringData) == 5: 325 tabSeperatedColumns, fontsize, letterSpace, shift, uppercase = stringData 326 else: 327 tabSeperatedColumns, fontsize, letterSpace, shift = stringData 328 uppercase = 0 329 tabSeperatedColumns = GetAsUnicode(tabSeperatedColumns) 330 columnTexts = tabSeperatedColumns.split('<t>') 331 columnTextsCount = len(columnTexts) 332 if columnTextsCount > tabCount: 333 tabstops.extend([minwidth] * (columnTextsCount - tabCount)) 334 tabCount = columnTextsCount 335 i = 0 336 for columnText in columnTexts: 337 width = max(0, shift - margin) + margin + self.GetTextWidth(columnText, fontsize, letterSpace, uppercase) + margin 338 if width > tabstops[i]: 339 tabstops[i] = width 340 i += 1 341 shift = 0 342 343 i = 1 344 while i < tabCount: 345 tabstops[i] += tabstops[i - 1] 346 i += 1 347 348 return tabstops 349 350 @telemetry.ZONE_METHOD 351 def MergeTabstops(self, tabs1, tabs2): 352 t1, t2 = self.GetTabstopsAsWidth(tabs1), self.GetTabstopsAsWidth(tabs2) 353 if not len(t2) and len(t1): 354 return tabs1 355 if not len(t1) and len(t2): 356 return tabs2 357 if not len(t1) and not len(t2): 358 return [] 359 newtabs = [max(t1[0], t2[0])] 360 for i in xrange(len(t1) - 1): 361 if len(t1) > i + 1: 362 _t1 = t1[i + 1] 363 else: 364 _t1 = 0 365 if len(t2) > i + 1: 366 _t2 = t2[i + 1] 367 else: 368 _t2 = 0 369 newtabs.append(newtabs[i] + max(_t1, _t2)) 370 371 return newtabs 372 373 @telemetry.ZONE_METHOD 374 def GetTabstopsAsWidth(self, tabs1): 375 tabs = tabs1[:1] 376 for i in xrange(len(tabs1) - 1): 377 tabs.append(tabs1[i + 1] - tabs1[i]) 378 379 return tabs 380 381 @telemetry.ZONE_METHOD 382 def GetNodeCopyData(self, node, fromIdx, toIdx): 383 glyphString = self.GetLineGlyphString(node) 384 if glyphString: 385 text = glyphString.GetText() 386 if toIdx == -1: 387 return text[fromIdx:] 388 else: 389 return text[fromIdx:toIdx] 390 return '' 391 392 def GetLineGlyphString(self, node): 393 return node.get('glyphString', None) 394 395 def GetLetterIdx(self, node, cursorXpos): 396 i = 0 397 glyphString = self.GetLineGlyphString(node) 398 if glyphString: 399 w = 0 400 for glyph in glyphString: 401 if w <= cursorXpos <= w + glyph[0]: 402 glyphCenter = glyph[0] / 2.0 403 if cursorXpos > w + glyphCenter: 404 return (i + 1, int(w + glyph[0])) 405 return (i, int(w)) 406 i += 1 407 w += glyph[0] 408 409 if cursorXpos > w: 410 return (i, int(w)) 411 return (0, 0) 412 413 def GetWidthToIdx(self, node, idx, getWidth = 0): 414 glyphString = self.GetLineGlyphString(node) 415 if glyphString: 416 if idx < len(glyphString): 417 w = sum([ t[0] for t in glyphString[:idx] ]) 418 if getWidth: 419 return (int(w), int(glyphString[idx][0])) 420 else: 421 return int(w) 422 else: 423 w = sum([ t[0] for t in glyphString ]) 424 if getWidth: 425 return (int(w), 0) 426 else: 427 return int(w) 428 if getWidth: 429 return (0, 0) 430 else: 431 return 0 432 433 @telemetry.ZONE_METHOD 434 def GetFontPathFromFontFamily(self, fontFamily, fontStyle = None, bold = False, italic = False): 435 fontPath = None 436 if type(fontFamily) == dict: 437 fontStyle = fontStyle or fontconst.STYLE_DEFAULT 438 if fontStyle in fontFamily: 439 fontFamily = fontFamily[fontStyle] 440 else: 441 fontFamily = fontFamily[fontconst.STYLE_DEFAULT] 442 if type(fontFamily) in types.StringTypes: 443 fontPath = fontFamily 444 elif type(fontFamily) in (tuple, list): 445 variation = bold * 2 + italic * 1 446 if len(fontFamily) > variation: 447 fontPath = fontFamily[variation] 448 else: 449 fontPath = fontFamily[0] 450 return fontPath or '' 451 452 @telemetry.ZONE_METHOD 453 def ResolveFontFamily(self, params): 454 if params.fontPath: 455 params.font = params.fontPath 456 return params.fontPath 457 fontFamily = params.fontFamily 458 if not fontFamily: 459 fontFamily = self.GetFontFamilyBasedOnClientLanguageID() 460 params.font = self.GetFontPathFromFontFamily(fontFamily, params.fontStyle, bool(params.bold), bool(params.italic)) 461 462 def GuessWindowsLanguageID(self, txt): 463 if txt in self.textLanguageIDCache: 464 return self.textLanguageIDCache[txt] 465 matches = [] 466 for languageID, windowsLanguageID, regex, isExclusive in self.regexes: 467 matchingLetters = len(txt) - len(regex.sub('', txt)) 468 if matchingLetters: 469 if isExclusive: 470 matches = [(matchingLetters, languageID, windowsLanguageID)] 471 break 472 else: 473 matches.append((matchingLetters, languageID, windowsLanguageID)) 474 if matchingLetters == len(txt): 475 break 476 477 matches.sort() 478 if matches: 479 self.textLanguageIDCache[txt] = matches[-1][-1] 480 return matches[-1][-1] 481 482 def GetWindowsLanguageIDForText(self, txt): 483 if txt in self.textLanguageIDCache: 484 return self.textLanguageIDCache[txt] 485 isLatinBased = self.IsLatinBased(txt) 486 if isLatinBased: 487 self.textLanguageIDCache[txt] = languageConst.LANG_ENGLISH 488 return languageConst.LANG_ENGLISH 489 guessedLanguageID = self.GuessWindowsLanguageID(txt) 490 self.textLanguageIDCache[txt] = guessedLanguageID 491 return guessedLanguageID 492 493 def IsLatinBased(self, txt): 494 for codepage in ('windows-1252', 'windows-1251', 'windows-1253', 'windows-1254', 'windows-1257', 'windows-1250'): 495 try: 496 txt.encode(codepage) 497 return True 498 except: 499 pass 500 501 def GetFontDefault(self): 502 family = self.GetFontFamilyBasedOnClientLanguageID()[fontconst.STYLE_DEFAULT] 503 regular, bold, italic, boldItalic = family 504 return regular 505 506 @telemetry.ZONE_METHOD 507 def GetFontFamilyBasedOnClientLanguageID(self): 508 if session: 509 languageID = session.languageID 510 else: 511 languageID = 'EN' 512 if languageID == 'ZH': 513 return self.GetFontFamilyBasedOnWindowsLanguageID(languageConst.LANG_CHINESE) 514 elif languageID == 'JA': 515 return self.GetFontFamilyBasedOnWindowsLanguageID(languageConst.LANG_JAPANESE) 516 elif languageID == 'KO': 517 return self.GetFontFamilyBasedOnWindowsLanguageID(languageConst.LANG_KOREAN) 518 else: 519 return self.GetFontFamilyBasedOnWindowsLanguageID(languageConst.LANG_ENGLISH) 520 521 @telemetry.ZONE_METHOD 522 def GetFontFamilyBasedOnWindowsLanguageID(self, windowsLanguageID): 523 if fontconst.FONT_FAMILY_OVERRIDE: 524 return fontconst.FONT_FAMILY_OVERRIDE 525 if windowsLanguageID in fontconst.LANGUAGES_USING_EVE_FONT: 526 return fontconst.FONTFAMILY_PER_WINDOWS_LANGUAGEID[languageConst.LANG_ENGLISH] 527 return fontconst.FONTFAMILY_PER_WINDOWS_LANGUAGEID.get(windowsLanguageID, None) 528 529 def GetCache(self, key): 530 cache = self.measureCache.get(key, None) 531 if cache: 532 return cache[1] 533 534 def SetCache(self, key, value): 535 ts = blue.os.GetWallclockTime() 536 self.measureCache[key] = (ts, value) 537 538 def InvalidateTextMeasureCache(self): 539 t = blue.os.GetWallclockTime() 540 ks = self.measureCache.keys() 541 for k in ks: 542 cacheTime = self.measureCache[k][0] 543 if t - cacheTime > 300000000: 544 del self.measureCache[k] 545 546 def GetParams(self): 547 fontParams = Bunch() 548 fontParams.__doc__ = 'Font Parameters' 549 fontParams.fontStyle = None 550 fontParams.fontFamily = None 551 fontParams.fontPath = None 552 fontParams.fontsize = fontconst.DEFAULT_FONTSIZE 553 fontParams.letterspace = fontconst.DEFAULT_LETTERSPACE or 0 554 fontParams.uppercase = fontconst.DEFAULT_UPPERCASE 555 fontParams.lineSpacing = 0.0 556 fontParams.wordspace = 0 557 fontParams.color = -1 558 fontParams.underline = 0 559 fontParams.bold = 0 560 fontParams.italic = 0 561 fontParams.face = None 562 return fontParams 563 564 def GetGlyphString(self): 565 return Tr2GlyphString() 566 567 def AppGetSetting(self, setting, default): 568 try: 569 return settings.public.ui.Get(setting, default) 570 except (AttributeError, NameError): 571 return default 572 573 def AppSetSetting(self, setting, value): 574 try: 575 settings.public.ui.Set(setting, value) 576 except (AttributeError, NameError): 577 pass