From d847825495418028e07f20fd485d31c67114a3a3 Mon Sep 17 00:00:00 2001 From: Mike Hiretsky Date: Thu, 20 Feb 2014 13:55:31 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=20=D0=B2=D1=8B=D0=B2=D0=BE=D0=B4=20=D0=BD=D0=B0=2016?= =?UTF-8?q?=D1=86=D0=B2=D0=B5=D1=82=D0=BD=D1=8B=D0=B9=20=D1=82=D0=B5=D1=80?= =?UTF-8?q?=D0=BC=D0=B8=D0=BD=D0=B0=D0=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- calculate/lib/utils/color/palette.py | 318 --------------- .../utils/{color => colortext}/__init__.py | 0 .../utils/{color => colortext}/converter.py | 31 +- .../lib/utils/{color => colortext}/output.py | 282 +++++++------ calculate/lib/utils/colortext/palette.py | 373 ++++++++++++++++++ setup.py | 2 +- 6 files changed, 556 insertions(+), 450 deletions(-) delete mode 100644 calculate/lib/utils/color/palette.py rename calculate/lib/utils/{color => colortext}/__init__.py (100%) rename calculate/lib/utils/{color => colortext}/converter.py (89%) rename calculate/lib/utils/{color => colortext}/output.py (53%) create mode 100644 calculate/lib/utils/colortext/palette.py diff --git a/calculate/lib/utils/color/palette.py b/calculate/lib/utils/color/palette.py deleted file mode 100644 index bfdbcd8..0000000 --- a/calculate/lib/utils/color/palette.py +++ /dev/null @@ -1,318 +0,0 @@ -#-*- 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 os -import sys -import re -from itertools import chain,ifilter,tee -from os import path - -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 - """ - if color and ConsoleColor256.webMatch.match(color): - color = [int(x,base=16) for x in (color[5:7],color[3:5],color[1:3])] - # 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 - # color 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 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): - if not color: - return self.defaultBackground - return self.colorMap.get(color,color) - - def getTextColor(self,color,bright): - """ - Получить соответствие цвета строкой - """ - if not color: - return self.defaultColor[bright] - color = self.brightTransform(color,bright) - return self.colorMap.get(color,color) - -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"])) - diff --git a/calculate/lib/utils/color/__init__.py b/calculate/lib/utils/colortext/__init__.py similarity index 100% rename from calculate/lib/utils/color/__init__.py rename to calculate/lib/utils/colortext/__init__.py diff --git a/calculate/lib/utils/color/converter.py b/calculate/lib/utils/colortext/converter.py similarity index 89% rename from calculate/lib/utils/color/converter.py rename to calculate/lib/utils/colortext/converter.py index 81ae107..fec602c 100644 --- a/calculate/lib/utils/color/converter.py +++ b/calculate/lib/utils/colortext/converter.py @@ -16,9 +16,9 @@ from output import BaseOutput from palette import (TextState, BaseColorMapping, ConsoleCodesInfo, - ConsoleCodeMapping, LightColorMapping, ConsoleColor256) + LightColorMapping, ConsoleColor256) from calculate.lib.utils.tools import SavableIterator -from itertools import chain,ifilter +from itertools import ifilter import re class ConsoleCodesConverter(object): @@ -34,7 +34,7 @@ class ConsoleCodesConverter(object): >>> cct = ConsoleCodesConverter(SpanCssOutput()) >>> outtext = "\033[32;1mHello\033[0;39m" >>> cct.transform(outtext) - 'Hello' + 'Hello' """ class CodeElement: @@ -85,8 +85,10 @@ class ConsoleCodesConverter(object): def __init__(self,output=None,escSymb="\033"): self.output = output or BaseOutput() self.escSymb = escSymb + self.escBlock = r"{esc}\[(\d+(?:;\d+)*)m".format(esc=escSymb) + self.reEscBlock = re.compile(self.escBlock) self.reParse = re.compile( - "({0}\[\d+(?:;\d+)*m)?(.*?)(?=$|{0}\[\d)".format(escSymb), + "(?:{0})?(.*?)(?=$|{0})".format(self.escBlock), re.DOTALL) resetBoldHalfbright = lambda : ( (self.output.resetBold() or "") + @@ -128,10 +130,9 @@ class ConsoleCodesConverter(object): Запустить преобразование текста """ def generator(): - offset = len(self.escSymb)+1 - for ctrl,txt in self.reParse.findall(s): + for ctrl,txt,_s in self.reParse.findall(s): if ctrl: - codes = SavableIterator(ctrl[offset:-1].split(';')) + codes = SavableIterator(ctrl.split(';')) for code in codes: code = int(code) res = "" @@ -146,18 +147,24 @@ class ConsoleCodesConverter(object): yield self.output.endText() return "".join(list(filter(None,generator()))) + def detect(self,s): + """ + Определить есть ли в тексте управляющие последовательности + """ + return bool(self.reEscBlock.search(s)) + class ConsoleCodes256Converter(ConsoleCodesConverter): """Расширяет возможность обработки 256 цветного терминала""" class Color256Element(ConsoleCodesConverter.CodeElement): - def __init__(self,action=lambda x:None, begin=None): + def __init__(self, action=lambda x: None, begin=None): self.action = action self.begin = begin - def tryParse(self,code): + def tryParse(self, code): return code == self.begin - def parse(self,code,codes): + def parse(self, code, codes): """ Тон: 38;5;0-255 Фон: 48;5;0-255 @@ -176,8 +183,8 @@ class ConsoleCodes256Converter(ConsoleCodesConverter): # если после 38 не 5 - не обрабатываем этот код codes.restore() - def __init__(self,*args,**kwargs): - ConsoleCodesConverter.__init__(self,*args,**kwargs) + def __init__(self, *args, **kwargs): + ConsoleCodesConverter.__init__(self, *args, **kwargs) cci = ConsoleCodesInfo # обработчики кодов для вывода в 256 foreground256 = self.Color256Element(begin=cci.FOREGROUND256, diff --git a/calculate/lib/utils/color/output.py b/calculate/lib/utils/colortext/output.py similarity index 53% rename from calculate/lib/utils/color/output.py rename to calculate/lib/utils/colortext/output.py index dc5af2b..822e632 100644 --- a/calculate/lib/utils/color/output.py +++ b/calculate/lib/utils/colortext/output.py @@ -14,18 +14,19 @@ # See the License for the specific language governing permissions and # limitations under the License. -from calculate.lib.utils.tools import SavableIterator from palette import (TextState, BaseColorMapping, - ConsoleCodeMapping, LightColorMapping, ConsoleColor256, - ConsoleCodesInfo, SpanPalette) + ConsoleCodeMapping, LightColorMapping, ConsoleColor256, + ConsoleCodesInfo, SpanPalette) -class BaseOutput: + +class BaseOutput(object): """ Базовый вывод текста. Вывод просто текста без изменения шрифта """ - def __init__(self,state=None): + + def __init__(self, state=None): pass def setBold(self): @@ -76,31 +77,31 @@ class BaseOutput: """ return "" - def outputText(self,text): + def outputText(self, text): """ Вывести текст с установленными настройками """ return text - def setForeground(self,color): + def setForeground(self, color): """ Установить цвет шрифта """ pass - def setBackground(self,color): + def setBackground(self, color): """ Установить цвет фона """ pass - def resetForeground(self,color): + def resetForeground(self, color): """ Использовать цвет шрифта по умолчанию """ pass - def resetBackground(self,color): + def resetBackground(self, color): """ Использовать цвет фона по умолчанию """ @@ -116,11 +117,13 @@ class BaseOutput: Выключить инверсию """ + class SaveAttrOutput(BaseOutput): """ Базовый класс с сохранением атрибутов """ - def __init__(self,state=None): + + def __init__(self, state=None): self.prev_state = state.clone() if state else TextState() self.current_state = self.prev_state.clone() @@ -150,13 +153,13 @@ class SaveAttrOutput(BaseOutput): self.resetBackground() self.resetInvert() - def setForeground(self,color): + def setForeground(self, color): self.current_state.foreground = color def resetForeground(self): self.current_state.foreground = TextState.Colors.DEFAULT - def setBackground(self,color): + def setBackground(self, color): self.current_state.background = color def resetBackground(self): @@ -168,17 +171,18 @@ class SaveAttrOutput(BaseOutput): def setInvert(self): self.current_state.invert = True + class ColorTerminalOutput(SaveAttrOutput): """ Форматирует текст для вывода в консоль """ mapColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND, - BaseColorMapping).mapTS_Console - mapLightColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND- - LightColorMapping.offset, - LightColorMapping).mapTS_Console + BaseColorMapping).mapTS_Console + mapLightColors = ConsoleCodeMapping(ConsoleCodesInfo.FOREGROUND - + LightColorMapping.offset, + LightColorMapping).mapTS_Console mapBackgroundColors = ConsoleCodeMapping(ConsoleCodesInfo.BACKGROUND, - BaseColorMapping).mapTS_Console + BaseColorMapping).mapTS_Console def setBold(self): self.resetHalfbright() @@ -188,101 +192,92 @@ class ColorTerminalOutput(SaveAttrOutput): self.resetBold() SaveAttrOutput.setHalfbright(self) - def handleForeground(self,prevstate,curstate,attrs): + def handleForeground(self, prevstate, curstate, attrs, tail_attrs): cci = ConsoleCodesInfo color = curstate.foreground if color in self.mapColors: attrs.append(self.mapColors[color]) elif color in self.mapLightColors: - if prevstate.bold == curstate.bold and curstate.bold == False: + if prevstate.bold == curstate.bold and not curstate.bold: + tail_attrs.append(cci.NORMAL) attrs.append(cci.BOLD) attrs.append(self.mapLightColors[color]) else: attrs.append(cci.FOREGROUND_DEFAULT) self.resetForeground() - def handleBackground(self,prevstate,curstate,attrs): + def handleBackground(self, prevstate, curstate, attrs, tail_attrs): color = curstate.background if color in self.mapBackgroundColors: attrs.append(self.mapBackgroundColors[color]) else: attrs.append(ConsoleCodesInfo.BACKGROUND_DEFAULT) - def handleIntensity(self,prevstate,curstate,attrs): + def handleIntensity(self, prevstate, curstate, attrs, tail_attrs): if curstate.bold and curstate.bold != prevstate.bold: attrs.append(ConsoleCodesInfo.BOLD) - elif (curstate.halfbright and - prevstate.halfbright != curstate.halfbright): + elif (curstate.halfbright and + prevstate.halfbright != curstate.halfbright): attrs.append(ConsoleCodesInfo.HALFBRIGHT) else: attrs.append(ConsoleCodesInfo.NORMAL) - - def handleUnderline(self,prevstate,curstate,attrs): + + def handleUnderline(self, prevstate, curstate, attrs, tail_attrs): if curstate.underline: attrs.append(ConsoleCodesInfo.UNDERLINE) else: attrs.append(ConsoleCodesInfo.NOUNDERLINE) - def handleInvert(self,prevstate,curstate,attrs): + def handleInvert(self, prevstate, curstate, attrs, tail_attrs): if curstate.invert: attrs.append(ConsoleCodesInfo.INVERT) else: attrs.append(ConsoleCodesInfo.NOINVERT) - def _createAttrs(self,prevstate,curstate): + def _createAttrs(self, prevstate, curstate): """ Создать ESC последовательность для установки параметров текста """ - attrs = [] + attrs, tail_attrs = [],[] # получить интенсивность (полутон и жирность относятся к интенсивности) - intensity = lambda x:x & (TextState.Attributes.HALFBRIGHT | - TextState.Attributes.BOLD) + intensity = lambda x: x & (TextState.Attributes.HALFBRIGHT | + TextState.Attributes.BOLD) - if (prevstate.attr != curstate.attr and - curstate.attr == TextState.Attributes.NONE and - curstate.foreground is TextState.Colors.DEFAULT and - curstate.background is TextState.Colors.DEFAULT): + if (prevstate.attr != curstate.attr and + curstate.attr == TextState.Attributes.NONE and + curstate.foreground is TextState.Colors.DEFAULT and + curstate.background is TextState.Colors.DEFAULT): attrs.append(ConsoleCodesInfo.RESET) else: if intensity(prevstate.attr) != intensity(curstate.attr): - self.handleIntensity(prevstate,curstate,attrs) + self.handleIntensity(prevstate, curstate, attrs, tail_attrs) if prevstate.underline != curstate.underline: - self.handleUnderline(prevstate,curstate,attrs) + self.handleUnderline(prevstate, curstate, attrs, tail_attrs) if prevstate.invert != curstate.invert: - self.handleInvert(prevstate,curstate,attrs) + self.handleInvert(prevstate, curstate, attrs, tail_attrs) if prevstate.foreground != curstate.foreground: - self.handleForeground(prevstate,curstate,attrs) + self.handleForeground(prevstate, curstate, attrs, tail_attrs) if prevstate.background != curstate.background: - self.handleBackground(prevstate,curstate,attrs) - return attrs + self.handleBackground(prevstate, curstate, attrs, tail_attrs) + return attrs, tail_attrs - def handlePostAttr(self,prevstate,curstate): - """ - Добавление аттрибутов после выведенного текста - """ - if prevstate.foreground != curstate.foreground: - color = curstate.foreground - if (color in self.mapLightColors and - prevstate.bold == curstate.bold and curstate.bold == False): - return [ConsoleCodesInfo.NORMAL] - - def _createEscCode(self,attrs): + def _createEscCode(self, attrs): """ Создать ESC строку """ - attrs = map(str,['\033['] + attrs + ['m']) - return "%s%s%s"%(attrs[0],";".join(attrs[1:-1]),attrs[-1]) + attrs = map(str, ['\033['] + attrs + ['m']) + return "%s%s%s" % (attrs[0], ";".join(attrs[1:-1]), attrs[-1]) - def outputText(self,s): + def outputText(self, s): """ Задание параметров текста и вывод его """ if self.prev_state != self.current_state: - attr = self._createAttrs(self.prev_state,self.current_state) + attr, tail_attrs = \ + self._createAttrs(self.prev_state, self.current_state) attr = self._createEscCode(attr) - postattr = self.handlePostAttr(self.prev_state,self.current_state) - if postattr: - postattr = self._createEscCode(postattr) + if tail_attrs: + postattr = self._createEscCode(tail_attrs) else: postattr = "" self.prev_state = self.current_state.clone() @@ -295,12 +290,14 @@ class ColorTerminalOutput(SaveAttrOutput): self.reset() return self.outputText("") + class ColorTerminal256Output(ColorTerminalOutput): """ Вывод на 256 цветный терминал """ mapLightColors = LightColorMapping.mapTS_Console - def handleForeground(self,prevstate,curstate,attrs): + + def handleForeground(self, prevstate, curstate, attrs, tail_attrs): color = curstate.foreground color256 = ConsoleColor256.rgbToConsole(color) if not color256 and color in self.mapLightColors: @@ -310,9 +307,10 @@ class ColorTerminal256Output(ColorTerminalOutput): ConsoleCodesInfo.COLOR256, color256]) else: - ColorTerminalOutput.handleForeground(self,prevstate,curstate,attrs) + ColorTerminalOutput.handleForeground(self, prevstate, curstate, + attrs, tail_attrs) - def handleBackground(self,prevstate,curstate,attrs): + def handleBackground(self, prevstate, curstate, attrs, tail_attrs): color = curstate.background color256 = ConsoleColor256.rgbToConsole(color) if not color256 and color in self.mapLightColors: @@ -322,75 +320,121 @@ class ColorTerminal256Output(ColorTerminalOutput): ConsoleCodesInfo.COLOR256, color256]) else: - ColorTerminalOutput.handleBackground(self,prevstate,curstate,attrs) + ColorTerminalOutput.handleBackground(self, prevstate, curstate, + attrs, tail_attrs) - def handlePostAttr(self,prevstate,curstate): - pass - -class SpanCssOutput(SaveAttrOutput): +class ColorTerminal16Output(ColorTerminalOutput): """ - Форматирует текст для вывода в консоль + Вывод на 16 цветный терминал с преобразованием RGB к ближайшему базовому """ - def __init__(self,state=None,palette=SpanPalette()): - SaveAttrOutput.__init__(self,state=state) + + def __init__(self, state=None, palette=None): + SaveAttrOutput.__init__(self, state=state) self.palette = palette - def getStringColor(self,color,bold=False,halfbright=False,background=False): + def _handleNearestColors(self, color): """ - Получить название цвета по номеру и состоянию текста + Обработка преобразования к ближайшему цвету """ - if halfbright: - bright = SpanPalette.LOW_BRIGHT - elif bold: - bright = SpanPalette.HIGH_BRIGHT - else: - bright = SpanPalette.NORMAL_BRIGHT + standardColors = TextState.normalColors + TextState.lightColors + if self.palette and color not in standardColors: + return self.palette.getBaseColorByRGB(color) + return color - if background: - return self.palette.getBackgroundColor(color) - else: - return self.palette.getTextColor(color,bright) + def handleForeground(self, prevstate, curstate, attrs, tail_attrs): + """ + Добавить преобразование RGB к ближайшему базовому + """ + _curstate = curstate.clone() + _curstate.foreground = \ + self._handleNearestColors(curstate.foreground) + super(ColorTerminal16Output, self).handleForeground( + prevstate, _curstate, + attrs, tail_attrs) - def getTags(self,prevstate,curstate): + def handleBackground(self, prevstate, curstate, attrs, tail_attrs): """ - Создать ESC последовательность для установки параметров текста + Добавить преобразование RGB к ближайшему базовому """ - style = [] - - colorAttr = ["color","background"] - if curstate.invert: - colorAttr = colorAttr[1],colorAttr[0] - if (prevstate.foreground != curstate.foreground or - prevstate.bold != curstate.bold or - curstate.invert or - prevstate.halfbright != curstate.halfbright): - sColor = self.getStringColor(curstate.foreground, - curstate.bold, - curstate.halfbright, - background=False) - style.append("%s:%s;"%(colorAttr[0],sColor)) - if prevstate.background != curstate.background or curstate.invert: - sColor = self.getStringColor(curstate.background,background=True) - style.append("%s:%s;"%(colorAttr[1],sColor)) - if prevstate.underline != curstate.underline: - if curstate.underline: - style.append("text-decoration:underline;") + mapHighNormal = dict(zip(TextState.lightColors, + TextState.normalColors)) + _curstate = curstate.clone() + _curstate.background = \ + self._handleNearestColors(curstate.background) + # преобразовать яркий цвет к обычному + _curstate.background = \ + mapHighNormal.get(_curstate.background, _curstate.background) + super(ColorTerminal16Output, self).handleBackground( + prevstate, _curstate, + attrs, tail_attrs) + +class SpanCssOutput(SaveAttrOutput): + """ + Форматирует текст для вывода в консоль + """ + + def __init__(self, state=None, palette=SpanPalette()): + SaveAttrOutput.__init__(self, state=state) + self.palette = palette + + def getStringColor(self, color, bold=False, halfbright=False, + background=False): + """ + Получить название цвета по номеру и состоянию текста + """ + if halfbright: + bright = SpanPalette.LOW_BRIGHT + elif bold: + bright = SpanPalette.HIGH_BRIGHT else: - style.append("text-decoration:none;") - if prevstate.bold != curstate.bold: - if curstate.bold: - style.append("font-weight:bold;") + bright = SpanPalette.NORMAL_BRIGHT + + if background: + return self.palette.getBackgroundColor(color) else: - style.append("font-weight:normal;") - return ''%"".join(style),'' + return self.palette.getTextColor(color, bright) - def outputText(self,s): - if self.prev_state != self.current_state: - lattr, rattr = self.getTags(self.prev_state,self.current_state) - else: - lattr = rattr = "" - return lattr + s + rattr + def getTags(self, prevstate, curstate): + """ + Создать ESC последовательность для установки параметров текста + """ + style = [] + + colorAttr = ["colortext", "background"] + if curstate.invert: + colorAttr = colorAttr[1], colorAttr[0] + if (prevstate.foreground != curstate.foreground or + prevstate.bold != curstate.bold or + curstate.invert or + prevstate.halfbright != curstate.halfbright): + sColor = self.getStringColor(curstate.foreground, + curstate.bold, + curstate.halfbright, + background=False) + style.append("%s:%s;" % (colorAttr[0], sColor)) + if prevstate.background != curstate.background or curstate.invert: + sColor = self.getStringColor(curstate.background, + background=True) + style.append("%s:%s;" % (colorAttr[1], sColor)) + if prevstate.underline != curstate.underline: + if curstate.underline: + style.append("text-decoration:underline;") + else: + style.append("text-decoration:none;") + if prevstate.bold != curstate.bold: + if curstate.bold: + style.append("font-weight:bold;") + else: + style.append("font-weight:normal;") + return '' % "".join(style), '' + + def outputText(self, s): + if self.prev_state != self.current_state: + lattr, rattr = self.getTags(self.prev_state, self.current_state) + else: + lattr = rattr = "" + return lattr + s + rattr - def endText(self): - self.reset() - return "" + def endText(self): + self.reset() + return "" diff --git a/calculate/lib/utils/colortext/palette.py b/calculate/lib/utils/colortext/palette.py new file mode 100644 index 0000000..ac8d926 --- /dev/null +++ b/calculate/lib/utils/colortext/palette.py @@ -0,0 +1,373 @@ +#-*- 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 + + +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 если подходящий цвет найден + """ + # TODO: исключить при приобразовании одинаковые цвет фона и тона + 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"])) diff --git a/setup.py b/setup.py index bd57c91..2466abf 100755 --- a/setup.py +++ b/setup.py @@ -44,7 +44,7 @@ setup( module_name + '.variables', module_name + '.mod', module_name + '.utils', - module_name + '.utils.color'], + module_name + '.utils.colortext'], data_files = [("/etc/calculate", []), ("/var/calculate/remote", []), ("/var/log/calculate", [])]