#-*- coding: utf-8 -*- # Copyright 2014 Calculate Ltd. http://www.calculate-linux.org # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import re from itertools import chain from math import pow from calculate.lib.utils.text import MultiReplace class ConsoleCodesInfo: """ Коды цветов """ COLORCOUNT = 8 BLACK, RED, GREEN, BROWN, BLUE, PURPLE, CYAN, WHITE = range(0, COLORCOUNT) DEFAULT = 9 FOREGROUND = 30 FOREGROUND_DEFAULT = 39 FOREGROUND_END = 37 BACKGROUND = 40 BACKGROUND_DEFAULT = 49 BACKGROUND_END = 47 BOLD = 1 HALFBRIGHT = 2 UNDERLINE = 4 NOUNDERLINE = 24 NORMAL = 22 RESET = 0 FOREGROUND256 = 38 COLOR256 = 5 BACKGROUND256 = 48 INVERT = 7 NOINVERT = 27 class ConsoleColor256: """Объект перевода RGB цветов в консоль 256color""" colorList = (0, 95, 135, 175, 215, 255) colorMatch = [colorList[x] + (colorList[x + 1] - colorList[x]) / 2 for x in range(0, 5)] + [255] colorHex = ["%02x" % val for val in colorList] webMatch = re.compile("^#[0-9A-Fa-f]{6}$") grayMatch = [6 + x * 11 for x in xrange(0, 23)] + [255] @staticmethod def rgbToConsole(color): """Перевести RGB в 256 консоль >>> ConsoleColor256.rgbToConsole("#5f00ff") 57 >>> ConsoleColor256.rgbToConsole("not #rrggbb") is None True """ color = ConsoleColor256.convertRgbToIntGroup(color) if color: # grayscale match if abs(color[0] - color[1]) + abs(color[0] - color[2]) < 5: for j, matchGray in enumerate(ConsoleColor256.grayMatch): if color[0] <= matchGray: return 232 + j # colortext match colorNumber = 16 for i, _color in enumerate(color): for j, halfColor in enumerate(ConsoleColor256.colorMatch): if _color <= halfColor: colorNumber += j * 6 ** i break return colorNumber return None @staticmethod def convertRgbToIntGroup(color): """Преобразовать #RRGGBB в кортеж целых (dec(RR),dec(GG),dec(BB)) В случае ошибки возвращает None """ if color and ConsoleColor256.webMatch.match(color): return [int(x, base=16) for x in (color[5:7], color[3:5], color[1:3])] return None @staticmethod def consoleToRgb(color): """ Перевести 256 консоль в #RGB >>> ConsoleColor256.consoleToRgb(216) '#ffaf87' >>> ConsoleColor256.consoleToRgb(15) is None True """ if color >= 255: return "#ffffff" if color >= 232: return "#{0:02x}{0:02x}{0:02x}".format((color - 232) * 11) elif color >= 16: color -= 16 return "#%s" % "".join(ConsoleColor256.colorHex[color / x % 6] for x in (36, 6, 1)) else: return None class TextState(object): """ Параметры текста >>> ts = TextState() >>> ts.attr = TextState.Attributes.BOLD | TextState.Attributes.UNDERLINE >>> ts.bold True >>> ts.underline True >>> ts.halfbright False """ class Attributes: NONE = 0 BOLD = 1 HALFBRIGHT = 2 UNDERLINE = 4 INVERT = 8 class Colors: # обычные цвета BLACK = "black" RED = "red" GREEN = "green" BROWN = "brown" BLUE = "blue" PURPLE = "purple" CYAN = "cyan" GRAY = "gray" # яркие цвета DARK = "dark" LIGHT_RED = "lightred" LIGHT_GREEN = "lightgreen" YELLOW = "yellow" LIGHT_BLUE = "lightblue" LIGHT_PURPLE = "lightpurple" LIGHT_CYAN = "lightcyan" WHITE = "white" # темные цвета (консоль пониженной яркости) DARK_BLACK = "darkblack" DARK_RED = "darkred" DARK_GREEN = "darkgreen" DARK_BROWN = "darkbrown" DARK_BLUE = "darkblue" DARK_PURPLE = "darkpurple" DARK_CYAN = "darkcyan" DARK_GRAY = "darkgray" DEFAULT = None normalColors = [Colors.BLACK, Colors.RED, Colors.GREEN, Colors.BROWN, Colors.BLUE, Colors.PURPLE, Colors.CYAN, Colors.GRAY] lightColors = [Colors.DARK, Colors.LIGHT_RED, Colors.LIGHT_GREEN, Colors.YELLOW, Colors.LIGHT_BLUE, Colors.LIGHT_PURPLE, Colors.LIGHT_CYAN, Colors.WHITE] darkColors = [Colors.DARK_BLACK, Colors.DARK_RED, Colors.DARK_GREEN, Colors.DARK_BROWN, Colors.DARK_BLUE, Colors.DARK_PURPLE, Colors.DARK_CYAN, Colors.DARK_GRAY] def bitProperty(bit): def set(self, value): self.attr &= ~bit self.attr |= bit if value else 0 def get(self): return bool(self.attr & bit) return property(get, set) # текст с подчеркиванием underline = bitProperty(Attributes.UNDERLINE) # текст жирный bold = bitProperty(Attributes.BOLD) # пониженная яркость halfbright = bitProperty(Attributes.HALFBRIGHT) # инверсия invert = bitProperty(Attributes.INVERT) def __init__(self, foreground=Colors.DEFAULT, background=Colors.DEFAULT, attr=Attributes.NONE): self.foreground = foreground self.background = background self.attr = attr def clone(self): return self.__class__(self.foreground, self.background, self.attr) def __cmp__(self, other): for i in ["foreground", "background", "attr"]: cmp_res = cmp(getattr(self, i), getattr(other, i)) if cmp_res: return cmp_res return 0 @classmethod def colors(cls): return chain(xrange(0, 8), [None]) class BaseColorMapping: # соответствие внутренних цветов консольным mapConsole_TS = {ConsoleCodesInfo.BLACK: TextState.Colors.BLACK, ConsoleCodesInfo.RED: TextState.Colors.RED, ConsoleCodesInfo.GREEN: TextState.Colors.GREEN, ConsoleCodesInfo.BROWN: TextState.Colors.BROWN, ConsoleCodesInfo.BLUE: TextState.Colors.BLUE, ConsoleCodesInfo.PURPLE: TextState.Colors.PURPLE, ConsoleCodesInfo.CYAN: TextState.Colors.CYAN, ConsoleCodesInfo.WHITE: TextState.Colors.GRAY} mapTS_Console = {v: k for k, v in mapConsole_TS.items()} def __init__(self, base): self.mapConsole_TS = self.mapConsole_TS.copy() self.mapConsole_TS.update(base.mapConsole_TS) self.mapTS_Console = {v: k for k, v in self.mapConsole_TS.items()} class LightColorMapping(BaseColorMapping): # соответствие внутренних цветов консольным offset = ConsoleCodesInfo.COLORCOUNT mapConsole_TS = {ConsoleCodesInfo.BLACK + offset: TextState.Colors.DARK, ConsoleCodesInfo.RED + offset: TextState.Colors.LIGHT_RED, ConsoleCodesInfo.GREEN + offset: TextState.Colors.LIGHT_GREEN, ConsoleCodesInfo.BROWN + offset: TextState.Colors.YELLOW, ConsoleCodesInfo.BLUE + offset: TextState.Colors.LIGHT_BLUE, ConsoleCodesInfo.PURPLE + offset: TextState.Colors.LIGHT_PURPLE, ConsoleCodesInfo.CYAN + offset: TextState.Colors.LIGHT_CYAN, ConsoleCodesInfo.WHITE + offset: TextState.Colors.WHITE} mapTS_Console = {v: k for k, v in mapConsole_TS.items()} class ConsoleCodeMapping(BaseColorMapping): """ Декоратор для преобразования кодов цвета в код цвета тона или фона Добавляет смещение к коду (3 -> 3 + codeoffset) """ def __init__(self, codeoffset, base): self.mapConsole_TS = {codeoffset + k: v for k, v in base.mapConsole_TS.items()} self.mapTS_Console = {v: k for k, v in self.mapConsole_TS.items()} class SpanPalette: """ Палитра для SpanCssOutput (черное на белом) """ LOW_BRIGHT, NORMAL_BRIGHT, HIGH_BRIGHT = 0, 1, 2 defaultColor = ["Black", "Black", "DarkGrey"] defaultBackground = "White" normalBright = dict(zip(TextState.normalColors, ["Black", "DarkRed", "DarkGreen", "Sienna", "DarkBlue", "DarkViolet", "LightSeaGreen", "Grey"])) highBright = dict(zip(TextState.lightColors, ["DarkGrey", "Red", "Green", "Yellow", "RoyalBlue", "Magenta", "Cyan", "White"])) lowBright = dict(zip(TextState.darkColors, ["Black", "Maroon", "DarkGreen", "SaddleBrown", "DarkBlue", "DarkViolet", "LightSeaGreen", "Grey"])) mapHighNormal = dict(zip(TextState.normalColors, TextState.lightColors)) mapLowNormal = dict(zip(TextState.normalColors, TextState.darkColors)) def __init__(self): self.colorMap = dict(self.normalBright.items() + self.highBright.items() + self.lowBright.items()) self.brightMap = {self.NORMAL_BRIGHT: {}, self.HIGH_BRIGHT: self.mapHighNormal, self.LOW_BRIGHT: self.mapLowNormal} def brightTransform(self, color, bright): """ Преобразовать основной цвет в зависимости от установленной яркости """ mapper = self.brightMap.get(bright, {}) return mapper.get(color, color) def getBackgroundColor(self, color=TextState.Colors.DEFAULT): if not color: return self.defaultBackground return self.colorMap.get(color, color) def getTextColor(self, color=TextState.Colors.DEFAULT, bright=NORMAL_BRIGHT): """ Получить соответствие цвета строкой """ if not color: return self.defaultColor[bright] color = self.brightTransform(color, bright) return self.colorMap.get(color, color) def getBaseColorByRGB(self, rgb_color): """Получить ближайший базовый цвет, согласно палитре Например: #709080 -> TextState.Colors.DARK Args: rgb_color: цвет #rrggbb (или словесный) Returns: TextState.Colors если подходящий цвет найден """ def calculate_color_diff(Ri,Ro,Gi,Go,Bi,Bo): # вычислить отличие цветов kR, kG, kB = 30, 59, 25 return kR*pow(Ri-Ro, 2)+kG*pow(Gi-Go, 2)+kB*pow(Bi-Bo, 2) color = ConsoleColor256.convertRgbToIntGroup(rgb_color) diffList = [] if color: for key, val in self.colorMap.items(): intVal = ConsoleColor256.convertRgbToIntGroup(val) if intVal: diffList.append(( calculate_color_diff(*chain(*zip(color, intVal))), key)) if diffList: #print diffList,sorted(diffList)[0] for diff, color in sorted(diffList): return color # если вместо RGB в палитре цвет указан словом elif rgb_color in self.colorMap.values(): for key, val in self.colorMap.items(): if val == rgb_color: return key return None class DarkPastelsPalette(SpanPalette): """ Палитра идентичная Calculate в консоли (Dark Pastels) """ defaultColor = ["#DCDCDC", "#DCDCDC", "#709080"] defaultBackground = "#2C2C2C" normalBright = dict(zip(TextState.normalColors, ["#2C2C2C", "#705050", "#60B48A", "#DFAF8F", "#9AB8D7", "#DC8CC3", "#8CD0D3", "#DCDCDC"])) highBright = dict(zip(TextState.lightColors, ["#709080", "#DCA3A3", "#72D5A3", "#F0DFAF", "#94BFF3", "#EC93D3", "#93E0E3", "#FFFFFF"])) class WhitePalette(SpanPalette): """ Палитра черное на белом """ defaultColor = ["#141414", "#141414", "#272727"] defaultBackground = "#ffffff" normalBright = dict(zip(TextState.normalColors, ["#141414", "#742523", "#237425", "#737423", "#252374", "#742373", "#237374", "#c1c1c1"])) highBright = dict(zip(TextState.lightColors, ["#272727", "#cc5755", "#55cc57", "#cacc55", "#5755cc", "#cc55ca", "#55cacc", "#ffffff"])) class GreyPaletteOld(SpanPalette): """ Палитра черное на светло-сером """ defaultColor = ["#101010", "#101010", "#505050"] defaultBackground = "#e4e1e0" normalBright = dict(zip(TextState.normalColors, ["#101010", "#902c2b", "#2b902c", "#8f902b", "#2c2b90", "#902b8f", "#2b8f90", "#b9b9b9"])) highBright = dict(zip(TextState.lightColors, ["#303030", "#c6403d", "#3dc640", "#c3c63d", "#403dc6", "#c63dc3", "#3dc3c6", "#e4e1e0"])) class GreyPalette(SpanPalette): """ Палитра черное на светло-сером """ defaultColor = ["#101010", "#101010", "#505050"] defaultBackground = "#ffffff" normalBright = dict(zip(TextState.normalColors, ["#101010", "#902c2b", "#2b902c", "#8f902b", "#2c2b90", "#902b8f", "#2b8f90", "#b9b9b9"])) highBright = dict(zip(TextState.lightColors, ["#303030", "#c6403d", "#3dc640", "#c3c63d", "#403dc6", "#c63dc3", "#3dc3c6", "#ffffff"])) class XmlFormat: """ Константы для внутреннего формата XML """ class Tags: BOLD = "b" UNDERLINE = "u" FONT = "font" HALFBRIGHT = "dark" INVERT = "invert" NEWLINE = "br" TAB = "tab" class FontAttributes: BACKGROUND = "bgColor" FOREGROUND = "color" escape_map = {'<': '<', '>': '>'} escaper = MultiReplace(escape_map) unescaper = MultiReplace({v: k for k, v in escape_map.items()})