Permalink
Fetching contributors…
Cannot retrieve contributors at this time
1307 lines (1135 sloc) 48.8 KB
# This file is part of Guacamole.
#
# Copyright 2012-2015 Canonical Ltd.
# Written by:
# Zygmunt Krynicki <zygmunt.krynicki@canonical.com>
#
# Guacamole is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License version 3,
# as published by the Free Software Foundation.
#
# Guacamole is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public License
# along with Guacamole. If not, see <http://www.gnu.org/licenses/>.
"""Ingredient for color transformations."""
from __future__ import absolute_import, print_function, unicode_literals
import argparse
import array
import gettext
import math
from guacamole.core import Ingredient
_ = gettext.gettext
_string_types = (str, type(""))
class ColorPalette(object):
"""
A palette that contains named colors.
Each color can be defined in up to three variants:
- RGB color
- 256-indexed color
- 8-indexed color
The first variant is only useful on modern terminal emulators on Linux.
Guacamole can automatically compute the indexed color for many other
terminal emulators. For best (and controllable) effects, application author
should supply a palette that supports at least the fist two color styles.
If your application is expected to work in a Windows environment, or the
Linux console then please also supply the third value.
"""
PREFER_TRUECOLOR, PREFER_INDEXED_256, PREFER_INDEXED_8 = range(3)
def __init__(self, **palette):
"""
Initialize a new palette.
:param palette:
A dictionary with color definitions (see below).
:raises ValueError:
If the palette is malformed.
For a discussion of how to define palette entries see
:meth:`add_colors()` below. The initializer uses it for everything.
"""
self._palette_rgb = {}
self._palette_i256 = {}
self._palette_i8 = {}
self.add_colors(**palette)
def add_colors(self, **palette):
"""
Add new colors to the palette.
:param palette:
A dictionary with color definitions (see below).
:raises ValueError:
If the palette is malformed.
The following definition formats are supported:
An integer in ``range(8)``:
The most widely supported color definition applicable to any
environment. The value maps to the eight well-known ANSI colors:
black=0, red=1, green=2, yellow=3, blue=4, magenta=5, cyan=6,
white=7.
An integer in ``range(256):
A more modern color applicable to nearly all Linux environments an
OS X. This mode is not supported on Windows. Distinct values have
the following meanings:
- range(8): same as the earlier definition
- range(8, 16): bright versions of earlier colors
- range(16, 256-24): 6 * 6 * 6 RGB values
- range(256-24, 25): 24 gray-scale values
A three-element tuple (r, g, b) with each component in range(256):
The most modern, true color definition. This version is only
supported on the most modern Linux terminal emulators. In Ubuntu
terms this is supported since, at least, Ubuntu 14.04.
A tuple of any of those:
This allows the designer to offer many variants of the same color
name that adapts correctly to the environment. This can be commonly
used to define both indexed and RGB color values.
"""
for name in palette:
name = str(name)
data = palette[name]
if isinstance(data, int) and data in range(256):
self._palette_i256[name] = data
if isinstance(data, int) and data < 8:
self._palette_i8[name] = data
elif isinstance(data, tuple) and len(data) == 3:
self._palette_rgb[name] = data
elif isinstance(data, tuple):
for sub_data in data:
if isinstance(sub_data, int) and sub_data in range(256):
self._palette_i256[name] = sub_data
if isinstance(sub_data, int) and sub_data < 8:
self._palette_i8[name] = sub_data
elif isinstance(sub_data, tuple) and len(sub_data) == 3:
self._palette_rgb[name] = sub_data
else:
raise ValueError("invalid color: {}".format(name))
else:
raise ValueError("invalid color: {}".format(name))
def resolve(self, color, prefer=PREFER_TRUECOLOR):
"""
Resolve a guacamole color definition.
:param color:
A named color (which is resolved according to the rules below). Or
an integer in range(256) that corresponds to the indexed palette
entry. Or an ``(r, g, b)`` triplet.
:param prefer:
Preferred color type. This can be ``PREFER_TRUECOLOR``,
``PREFER_INDEXED_256`` or ``PREFER_INDEXED_8``. See below for
details.
:returns:
The resolved color. See below.
:raises ValueError:
If the color format is unrecognized
"""
if isinstance(color, _string_types):
if prefer == self.PREFER_TRUECOLOR:
try:
return self._palette_rgb[color]
except KeyError:
pass
try:
return self._palette_i256[color]
except KeyError:
pass
try:
return self._palette_i8[color]
except KeyError:
pass
elif prefer == self.PREFER_INDEXED_256:
try:
return self._palette_i256[color]
except KeyError:
pass
try:
return self._palette_i8[color]
except KeyError:
pass
elif prefer == self.PREFER_INDEXED_8:
try:
return self._palette_i8[color]
except KeyError:
pass
raise LookupError("unknown named color: {!r}".format(color))
elif isinstance(color, int):
if color not in range(256):
raise ValueError("indexed colors are defined in range(256)")
return color
elif isinstance(color, tuple):
return color
else:
raise ValueError(
"Unsupported color representation: {!r}".format(color))
class TerminalPalette(object):
"""
Palette of a particular terminal emulator.
Various terminal emulators render the set of 256 palette-based colors
differently. In order to implement color transformations each of those
palette entries must be resolvable to an RGB triplet.
"""
def __init__(self, palette, slug, name):
"""
Initialize a new terminal palette.
:param palette:
A tuple of 256 triplets ``(r, g, b)`` where each component is
an integer in ``range(256)``
:param slug:
A short name of the palette. This will be visible on command line
:param name:
A human-readable name of the palette. This will be visible in
certain parts of the user interface.
"""
if len(palette) != 256:
raise ValueError("The palette needs to have 256 entries")
self.palette = palette
self.slug = slug
self.name = name
def __str__(self):
"""Get the name of the terminal emulator palette."""
return self.name
def __repr__(self):
"""Get the debugging representation of a terminal emulator palette."""
return "<{0} {1}>".format(self.__class__.__name__, self.slug)
def resolve(self, color):
"""
Resolve a symbolic or indexed color to an ``(r, g, b)`` triplet.
:param color:
An integer in range(256) that corresponds to the indexed palette
entry. Or an ``(r, g, b)`` triplet.
:returns:
An ``(r, g, b)`` triplet that corresponds to the given color.
:raises ValueError:
If the color format is unrecognized.
:raises ValueError:
When a color name is used. Please use a :class:`ColorPalette` to
resolve it before continuing with the terminal palette.
"""
if isinstance(color, _string_types):
raise ValueError("color names are not supported"
", please use the palette to resolve them")
elif isinstance(color, int):
try:
return self.palette[color]
except IndexError:
raise ValueError("indexed colors are defined in range(256)")
elif isinstance(color, tuple):
return color
else:
raise ValueError(
"Unsupported color representation: {!r}".format(color))
def resolve_fast(self, color):
"""
Resolve a symbolic or indexed color to an ``(r, g, b)`` triplet.
:param color:
An integer in range(256) that corresponds to the indexed palette
entry. Or an ``(r, g, b)`` triplet.
:returns:
An ``(r, g, b)`` triplet that corresponds to the given color.
:raises ValueError:
If the color format is unrecognized.
:raises ValueError:
When a color name is used. Please use a :class:`ColorPalette` to
resolve it before continuing with the terminal palette.
"""
try:
return self.palette[color]
except IndexError:
raise ValueError("indexed colors are defined in range(256)")
#: A palette with (r, g, b) triplets (using 0-255 integer range) for each
# of the symbolic color names.
# This palette was sampled on Ubuntu 15.04 (text mode). Note that only
# "bold/bright" variant of the bright colors are available.
LinuxConsolePalette = TerminalPalette((
# Basic colors
(0x00, 0x00, 0x00), # 0x00 - black
(0xaa, 0x00, 0x00), # 0x01 - red
(0x00, 0xaa, 0x00), # 0x02 - green
(0xaa, 0x55, 0x00), # 0x03 - yellow
(0x00, 0x00, 0xaa), # 0x04 - blue
(0xaa, 0x00, 0xaa), # 0x05 - magenta
(0x00, 0xaa, 0xaa), # 0x06 - cyan
(0xaa, 0xaa, 0xaa), # 0x07 - white
# Bright colors
(0x55, 0x55, 0x55), # 0x08 - bright black
(0xff, 0x55, 0x55), # 0x09 - bright red
(0x55, 0xff, 0x55), # 0x0A - bright green
(0xff, 0xff, 0x55), # 0x0B - bright yellow
(0x55, 0x55, 0xff), # 0x0C - bright blue
(0xff, 0x55, 0xff), # 0x0D - bright magenta
(0x55, 0xff, 0xff), # 0x0E - bright cyan
(0xff, 0xff, 0xff), # 0x0F - bright white
) + ((0xaa, 0xaa, 0xaa),) * (256 - 16), # (all of the rest is plain)
"linux-console", _("Linux Console"))
_rgb6 = (0x00, 0x5f, 0x87, 0xaf, 0xd7, 0xff)
#: Palette sampled on Ubuntu 15.04 using Gnome Terminal with "custom"
# color scheme (default for Ubuntu).
GnomeTerminalUbuntu1504Palette = TerminalPalette((
# Basic colors
(0x2e, 0x34, 0x36), # 0x00 - black
(0xcc, 0x00, 0x00), # 0x01 - red
(0x4e, 0x9a, 0x06), # 0x02 - green
(0xc4, 0xa0, 0x00), # 0x03 - yellow
(0x34, 0x65, 0xa4), # 0x04 - blue
(0x75, 0x50, 0x7b), # 0x05 - magenta
(0x06, 0x98, 0x9a), # 0x06 - cyan
(0xd3, 0xd7, 0xcf), # 0x07 - white
# Bright colors
(0x55, 0x57, 0x53), # 0x08 - bright black
(0xef, 0x29, 0x29), # 0x09 - bright red
(0x8a, 0xe2, 0x34), # 0x0A - bright green
(0xfc, 0xe9, 0x4f), # 0x0B - bright yellow
(0x72, 0x9f, 0xcf), # 0x0C - bright blue
(0xad, 0x7f, 0xa8), # 0x0D - bright magenta
(0x34, 0xe2, 0xe2), # 0x0E - bright cyan
(0xee, 0xee, 0xec), # 0x0F - bright white
#: 6 * 6 * 6 RGB colors
) + tuple(
(_rgb6[r], _rgb6[g], _rgb6[b])
for r in range(6)
for g in range(6)
for b in range(6)
#: 24 gray-scale colors
) + tuple(
(shade * 0x0A + 0x08,) * 3
for shade in range(24)
), "gnome-terminal-ubuntu-15.04-default",
_("Gnome Terminal on Ubuntu 15.04 (default)"))
#: Palette sampled on Ubuntu 15.04 using xterm
XTermPalette = TerminalPalette((
# Basic colors
(0x2e, 0x34, 0x36), # 0x00 - black
(0xcc, 0x00, 0x00), # 0x01 - red
(0x4e, 0x9a, 0x06), # 0x02 - green
(0xc4, 0xa0, 0x00), # 0x03 - yellow
(0x34, 0x65, 0xa4), # 0x04 - blue
(0x75, 0x50, 0x7b), # 0x05 - magenta
(0x06, 0x98, 0x9a), # 0x06 - cyan
(0xd3, 0xd7, 0xcf), # 0x07 - white
# Bright colors
(0x55, 0x57, 0x53), # 0x08 - bright black
(0xef, 0x29, 0x29), # 0x09 - bright red
(0x8a, 0xe2, 0x34), # 0x0A - bright green
(0xfc, 0xe9, 0x4f), # 0x0B - bright yellow
(0x72, 0x9f, 0xcf), # 0x0C - bright blue
(0xad, 0x7f, 0xa8), # 0x0D - bright magenta
(0x34, 0xe2, 0xe2), # 0x0E - bright cyan
(0xee, 0xee, 0xec), # 0x0F - bright white
#: 6 * 6 * 6 RGB colors
) + tuple(
(_rgb6[r], _rgb6[g], _rgb6[b])
for r in range(6)
for g in range(6)
for b in range(6)
#: 24 gray-scale colors
) + tuple(
(shade * 0x0A + 0x08,) * 3
for shade in range(24)
), "xterm-256color",
_("X Terminal Emulator"))
# TODO: putty palette
#: Palette sampled on Mac OS X 10.10 (yosemite) Apple Terminal
AppleTerminalOSX1010Palette = TerminalPalette((
# Basic colors
(0x00, 0x00, 0x00), # 0x00 - black
(0x99, 0x00, 0x00), # 0x01 - red
(0x1a, 0x50, 0x06), # 0x02 - green
(0xc4, 0xa0, 0x00), # 0x03 - yellow
(0x34, 0x65, 0xa4), # 0x04 - blue
(0x75, 0x50, 0x7b), # 0x05 - magenta
(0x06, 0x98, 0x9a), # 0x06 - cyan
(0xd3, 0xd7, 0xcf), # 0x07 - white
# Bright colors
(0x55, 0x57, 0x53), # 0x08 - bright black
(0xef, 0x29, 0x29), # 0x09 - bright red
(0x8a, 0xe2, 0x34), # 0x0A - bright green
(0xfc, 0xe9, 0x4f), # 0x0B - bright yellow
(0x72, 0x9f, 0xcf), # 0x0C - bright blue
(0xad, 0x7f, 0xa8), # 0x0D - bright magenta
(0x34, 0xe2, 0xe2), # 0x0E - bright cyan
(0xee, 0xee, 0xec), # 0x0F - bright white
#: 6 * 6 * 6 RGB colors
) + tuple(
(_rgb6[r], _rgb6[g], _rgb6[b])
for r in range(6)
for g in range(6)
for b in range(6)
#: 24 gray-scale colors
) + tuple(
(shade * 0x0A + 0x08,) * 3
for shade in range(24)
), "apple-terminal-osx-10.10-default",
_("Apple Terminal on OS X 10.10 (default)"))
class AccessibilityEmulator(object):
"""
An accessibility emulator for color-blind user experience.
This class can emulate various kinds of color blindness. The particular
color transformation matrix is stored in the emulator. Any ``(r, g, b)``
color can be transformed to the resulting approximation of how a given
class of color-blind people would perceive that color.
The way this works is by associating three coefficients for each of
``(r, g, b)`` values. The resulting color is then mixed with each basic
colors in different quantities.
The resulting colors are computed according to this formula::
red = red_c.r * red + green_c.green * green + blue_c.blue * blue
(similar for green and blue)
For example a non-color-blind person would have the following
coefficients::
red_c = (1, 0, 0)
green_c = (0, 1, 0)
blue_c = (0, 0, 1)
This simply results in an identity transformation of the original color (no
change occurs). A person affected by protanopia would see different colors,
those can be emulated with this set of coefficients::
red_c = (0.56667, 0.43333, 0)
green_c = (0.55833, 0.44167, 0)
blue_c = (0, 0.24167, 0.75833)
Here the effective red color would be a 56% mixture of input red and 43% of
input green. The effective green would be a similar mixture of 55% red and
44% green. Lastly, the effective blue would be a 24% mixture of green and
75% blue.
"""
def __init__(self, matrix, slug, name):
"""
Initialize a new accessibility emulator with a given matrix and name.
:param matrix:
Any sequence of 9 floating point values. They are distributed to
the coefficients according to this pattern: ``(red_red, red_green,
red_blue, green_red, green_green, green_blue, blue_red, blue_green,
blue_blue)`` Each coefficient must be a number between zero and
one. Coefficients from each basic color should add up to one.
:param name:
A human-readable name of the accessibility emulator.
"""
matrix = array.array(str('f'), matrix)
if len(matrix) != 9:
raise ValueError("matrix must have 9 elements")
if any(0 > factor > 1 for factor in matrix):
raise ValueError("all factors must be in range 0..1")
self.matrix = matrix
self.name = name
self.slug = slug
def __str__(self):
"""Get the name of the accessibility emulator."""
return self.name
def __repr__(self):
"""Get the debugging representation of an accessibility emulator."""
return "<{0} {1}>".format(self.__class__.__name__, self.slug)
def transform(self, r, g, b):
"""Transform the input color according to the matrix."""
m = self.matrix
# TODO: optimize this code
tr = m[0 * 3 + 0] * r + m[0 * 3 + 1] * g + m[0 * 3 + 2] * b
tg = m[1 * 3 + 0] * r + m[1 * 3 + 1] * g + m[1 * 3 + 2] * b
tb = m[2 * 3 + 0] * r + m[2 * 3 + 1] * g + m[2 * 3 + 2] * b
ro, go, bo = int(min(255, tr)), int(min(255, tg)), int(min(255, tb))
return ro, go, bo
Normal = AccessibilityEmulator(
[1, 0, 0,
0, 1, 0,
0, 0, 1],
"normal", _("Normal"))
GrayScale1 = AccessibilityEmulator(
[0.3, 0.59, 0.11,
0.3, 0.59, 0.11,
0.3, 0.59, 0.11],
"gray-scale-intensity", _("Gray scale (intensity)"))
GrayScale2 = AccessibilityEmulator(
[0.3333, 0.3333, 0.3333,
0.3333, 0.3333, 0.3333,
0.3333, 0.3333, 0.3333],
"gray-scale-average", _("Gray scale (average)"))
Protanopia = AccessibilityEmulator(
[0.56667, 0.43333, 0,
0.55833, 0.44167, 0,
0, 0.24167, 0.75833],
"protanopia", _("Protanopia"))
Protanomaly = AccessibilityEmulator(
[0.81667, 0.18333, 0,
0.33333, 0.66667, 0,
0, 0.125, 0.875],
"protanomaly", _("Protanomaly"))
Deuteranopia = AccessibilityEmulator(
[0.625, 0.375, 0,
0.70, 0.3, 0,
0, 0.30, 0.70],
"deuteranopia", _("Deuteranopia"))
Deuteranomaly = AccessibilityEmulator(
[0.80, 0.20, 0,
0.25833, 0.74167, 0,
0, 0.14167, 0.85833],
"deuteranomaly", _("Deuteranomaly"))
Tritanopia = AccessibilityEmulator(
[0.95, 0.5, 0,
0, 0.43333, 0.56667,
0, 0.475, 0.525],
"tritanopia", _("Tritanopia"))
Tritanomaly = AccessibilityEmulator(
[0.96667, 0.3333, 0,
0, 0.73333, 0.26667,
0, 0.18333, 0.81667],
"tritanomaly", _("Tritanomaly"))
Achromatopsia = AccessibilityEmulator(
[0.299, 0.587, 0.114,
0.299, 0.587, 0.114,
0.299, 0.587, 0.114],
"achromatopsia", _("Achromatopsia"))
Achromatomaly = AccessibilityEmulator(
[0.618, 0.32, 0.62,
0.163, 0.775, 0.62,
0.163, 0.320, 0.516],
"achromatomaly", _("Achromatomaly"))
class ColorMixerBase(object):
"""
A filter for color post-processing.
This class is essentially a function mapping colors to colors. It can be
used to implement the RGB-to-indexed downmixing that is required on many
terminal emulators.
:attr slug:
The non-translatable identifier of this mixer.
:attr name:
A human-readable name of the accessibility emulator.
:attr preferred_mode:
The preferred named color resolution mode. This is either
ColorPalette.PREFER_TRUECOLOR, ColorPalette.PREFER_INDEXED_256 or
ColorPalette.PREFER_INDEXED_8.
"""
def __str__(self):
"""Get the name of a color mixer."""
return self.name
def __repr__(self):
"""Get the debugging representation of a color mixer."""
return "<{0} {1}>".format(self.__class__.__name__, self.slug)
def mix(self, r, g, b, terminal_palette):
"""
Optionally downmix the input color to an indexed color.
:param r:
The red component. It is always an integer in ``range(256)``.
:param g:
The blue component. It is always an integer in ``range(256)``.
:param b:
The green component. It is always an integer in ``range(256)``.
:param terminal_palette:
An array / tuple of exactly 256 entries. Each entry maps to a
3-tuple ``(r, g, b)`` that describes the actual color used by the
terminal emulator to render the corresponding indexed color.
:returns:
The downmixed color. It can be the either the ``(r, g, b)`` input
color or a new integer in ``range(256)`` that corresponds to the
input color.
"""
raise NotImplementedError
# NOTE: The ColorController has a fast path for the pass-through case. It is
# best not to actually use the PassthruColorMixer
class TrueColorMixer(ColorMixerBase):
"""
True Color mixer.
This color mixer simply returns the r, g, b data without reinterpretation.
This mixer uses PREFER_TRUECOLOR colors from the named color palette so
that applications can have full fidelity in expressing the desired color.
"""
name = _("24bit RGB (TrueColor)")
slug = 'truecolor'
preferred_mode = ColorPalette.PREFER_TRUECOLOR
needs_palette = False
def mix(self, r, g, b, terminal_palette):
"""
Optionally downmix the input color to an indexed color.
:param r:
The red component. It is always an integer in ``range(256)``.
:param g:
The blue component. It is always an integer in ``range(256)``.
:param b:
The green component. It is always an integer in ``range(256)``.
:param terminal_palette:
An array / tuple of exactly 256 entries. Each entry maps to a
3-tuple ``(r, g, b)`` that describes the actual color used by the
terminal emulator to render the corresponding indexed color.
:returns:
The downmixed color. It can be the either the ``(r, g, b)`` input
color or a new integer in ``range(256)`` that corresponds to the
input color.
.. note::
This implementation always returns ``(r, g, b)`` unchanged.
"""
return (r, g, b)
class FastIndexed256ColorMixer(ColorMixerBase):
"""
Fast mixer that reduces true color to one of 246 indexed values.
This mixer produces sub-optimal results because it ignores the real
terminal emulator palette and simply assumes a given arrangement of colors.
The mixer has two code paths, one is optimized for grayscale colors, the
other for saturated colors.
The grayscale path is used when all of the color channels have the same
value. In that mode, the mixer uses one of the 24 linear grayscale values
as the output.
Int the saturated color path the mixer uses one of the 216 available RGB
colors from the 6*6*6 color cube. It operates by dividing each channel by
256/6.0 which is 42.(6), clamping that to range(6) then re-mapping the
result to the 6*6*6 color cube.
It has the advantage of not requiring any lookup tables and producing
colors quickly but the perception is somewhat suboptimal.
This mixer uses PREFER_INDEXED_256 colors from the named color palette so
that applications can avoid the inaccurate conversion if they provide a
hand-picked indexed color variant for each named color.
"""
name = _("Indexed 256 color palette (fast transform)")
slug = 'fast-indexed-256'
preferred_mode = ColorPalette.PREFER_INDEXED_256
needs_palette = False
def mix(self, r, g, b, terminal_palette):
"""
Optionally downmix the input color to an indexed color.
:param r:
The red component. It is always an integer in ``range(256)``.
:param g:
The blue component. It is always an integer in ``range(256)``.
:param b:
The green component. It is always an integer in ``range(256)``.
:param terminal_palette:
An array / tuple of exactly 256 entries. Each entry maps to a
3-tuple ``(r, g, b)`` that describes the actual color used by the
terminal emulator to render the corresponding indexed color.
:returns:
The downmixed color. It can be the either the ``(r, g, b)`` input
color or a new integer in ``range(256)`` that corresponds to the
input color.
.. note::
This implementation always returns an indexed color.
"""
# To avoid coloring grayscale colors, include a special case
# grayscale path if all the components are of the same magnitude.
if r == g == b:
# NOTE: 0xE8 is the offset of the 24 grayscale bar in the ANSI
# indexed color palette.
color = 0xE8 + int(r / (256.0 / 24))
assert color in range(0xE8, 0x100)
return color
else:
f = 256 / 6.0
r /= f
g /= f
b /= f
r = int(r)
g = int(g)
b = int(b)
r = max(0, min(r, 5))
g = max(0, min(g, 5))
b = max(0, min(b, 5))
# NOTE: 0x10 is the offset of the 6 * 6 * 6 color cube in the ANSI
# indexed color palette.
assert r in range(6)
assert g in range(6)
assert b in range(6)
assert r * 36 + g * 6 + b in range(216)
color = 0x10 + r * 36 + g * 6 + b
assert color in range(0x10, 0xE8)
return color
class FastIndexed8ColorMixer(ColorMixerBase):
"""
Mixer that reduces true color to one of 8 classic indexed values.
This mixer produces rather terrible results simply because of the bare
minimum combination of available output colors. It can be used to
downconvert a full-color application to DOS or Linux Console environment.
It is strongly recommended that applications use optimized, hand-picked
colors however, as otherwise the application may be a colorful, perhaps
unreadable mess.
It operates by reducing the bit depth of each color channel to one.
This mixer uses PREFER_INDEXED_8 colors so that applications can take
advantage of optimized, low-color palette entries for named colors.
"""
name = _("Indexed 8 color palette (fast transform)")
slug = 'fast-indexed-8'
preferred_mode = ColorPalette.PREFER_INDEXED_8
needs_palette = False
def mix(self, r, g, b, terminal_palette):
"""
Optionally downmix the input color to an indexed color.
:param r:
The red component. It is always an integer in ``range(256)``.
:param g:
The blue component. It is always an integer in ``range(256)``.
:param b:
The green component. It is always an integer in ``range(256)``.
:param terminal_palette:
An array / tuple of exactly 256 entries. Each entry maps to a
3-tuple ``(r, g, b)`` that describes the actual color used by the
terminal emulator to render the corresponding indexed color.
:returns:
The downmixed color. It can be the either the ``(r, g, b)`` input
color or a new integer in ``range(256)`` that corresponds to the
input color.
.. note::
This implementation always returns an indexed color.
"""
color = ((r >> 7) << 2) | ((g >> 7)) << 1 | (b >> 7)
assert color in range(8)
return color
class AccurateIndexed256ColorMixer(ColorMixerBase):
"""
Accurate mixer that reduces true color to one of 256 indexed values.
This mixer produces pretty good results as it considers each of the 256
palette entries. It is significantly slower than its cousin
:class:`FastIndexed256ColorMixer` so depending on the application you may
not want to use it.
It operates by looking for minimal distance between the input color and
each of the colors of the terminal emulator palette. Unlike other mixers it
does take advantage of the terminal palette to improve accuracy of the
conversion. This means that the resulting color is may be different from
one system to another but the perceived color should be the same.
This mixer uses PREFER_INDEXED_256 colors from the named color palette so
that applications can avoid the costly conversion if they provide a
hand-picked indexed color variant for each named color.
"""
name = _("Indexed 256 color palette (accurate transform)")
slug = 'accurate-indexed-256'
preferred_mode = ColorPalette.PREFER_INDEXED_256
needs_palette = True
def mix(self, r, g, b, terminal_palette):
"""
Optionally downmix the input color to an indexed color.
:param r:
The red component. It is always an integer in ``range(256)``.
:param g:
The blue component. It is always an integer in ``range(256)``.
:param b:
The green component. It is always an integer in ``range(256)``.
:param terminal_palette:
An array / tuple of exactly 256 entries. Each entry maps to a
3-tuple ``(r, g, b)`` that describes the actual color used by the
terminal emulator to render the corresponding indexed color.
:returns:
The downmixed color. It can be the either the ``(r, g, b)`` input
color or a new integer in ``range(256)`` that corresponds to the
input color.
.. note::
This implementation always returns an indexed color.
"""
min_distance = 1000
min_distance_idx = 0
for idx, (r2, g2, b2) in enumerate(terminal_palette):
distance = math.sqrt((r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2)
if distance == 0:
return idx
elif distance < min_distance:
min_distance = distance
min_distance_idx = idx
assert min_distance_idx in range(0x100)
return min_distance_idx
class AccurateIndexed8ColorMixer(ColorMixerBase):
"""
Mixer that reduces true color to one of 8 classic indexed values.
This mixer produces rather terrible results simply because of the bare
minimum combination of available output colors. It can be used to
downconvert a full-color application to DOS or Linux Console environment.
It is strongly recommended that applications use optimized, hand-picked
colors however, as otherwise the application may be a colorful, perhaps
unreadable mess.
It operates by looking for minimal distance between the input color and
each of the eight colors of the terminal emulator palette. To avoid
creating unexpected colors, it has a special case when all of the base
components have the same magnitude. In that mode it will simply output
black or white, depending on the threshold.
This mixer uses PREFER_INDEXED_8 colors so that applications can take
advantage of optimized, low-color palette entries for named colors.
"""
name = _("Indexed 8 color palette (accurate transform)")
slug = 'accurate-indexed-8'
preferred_mode = ColorPalette.PREFER_INDEXED_8
needs_palette = False
def mix(self, r, g, b, terminal_palette):
"""
Optionally downmix the input color to an indexed color.
:param r:
The red component. It is always an integer in ``range(256)``.
:param g:
The blue component. It is always an integer in ``range(256)``.
:param b:
The green component. It is always an integer in ``range(256)``.
:param terminal_palette:
An array / tuple of exactly 256 entries. Each entry maps to a
3-tuple ``(r, g, b)`` that describes the actual color used by the
terminal emulator to render the corresponding indexed color.
:returns:
The downmixed color. It can be the either the ``(r, g, b)`` input
color or a new integer in ``range(256)`` that corresponds to the
input color.
.. note::
This implementation always returns an indexed color.
"""
color = ((r >> 7) << 2) | ((g >> 7)) << 1 | (b >> 7)
assert color in range(8)
return color
# To avoid coloring grayscale colors, include a special case
# (black/white) exit path if all the components are of the same
# magnitude.
if r == g == b:
return 7 if r > 127 else 0
min_distance = 1000
min_distance_idx = 0
for idx, (r2, g2, b2) in enumerate(terminal_palette[:8]):
distance = math.sqrt((r - r2) ** 2 + (g - g2) ** 2 + (b - b2) ** 2)
if distance == 0:
return idx
elif distance < min_distance:
min_distance = distance
min_distance_idx = idx
assert min_distance_idx in range(0x100)
return min_distance_idx
def get_intensity(r, g, b):
"""
Get the gray level intensity of the given rgb triplet.
:param rgb:
A tuple ``(r, g, b)`` where each component is an integer in
``range(256)``.
:returns:
An integer in ``range(256)`` which represents the intensity
(brightness) of the input color.
"""
return int(0.3 * r + 0.59 * g + 0.11 * b)
class ColorController(object):
"""
Controller for all the color usage inside Guacamole.
The controller acts as a global color interpreter inside any Guacamole
application. By default it is disabled. In this mode, any color used by the
application is directly used without change.
The controller can be enabled by toggling the :attr:`active` attribute. In
that mode, the :meth:`transform()` performs two operations, first of all,
all symbolic and indexed colors are replaced by their RGB counterparts.
Then, each of the colors is further modified according to the active
accessibility emulator used.
This operation requires that the palette and accessibility emulators are
set using :meth:`accessibility_emulator` and :meth:`terminal_palette`
respectively.
Unfortunately each terminal emulator uses different palette and Guacamole
can only try to catch up with the commonly used palettes.
You can use the method :meth:`get_available_terminal_palettes()` to learn
about all available palettes. Similarly you can use
:meth:`get_available_accessibility_emulators()` to discover all
accessibility emulators. Each of the returned objects has a human-readable
name attribute that can be used for user interface. If you application
chooses to provide one.
"""
# Color rendering mode
MODE_RGB, MODE_INDEXED = range(2)
def __init__(self):
"""Initialize a new color controller."""
self._active = False
self._color_palette = ColorPalette()
self._terminal_palette = None
self._color_mixer = None
self._accessibility_emulator = None
# Basic ANSI colors
self.add_colors(
black=0, red=1, green=2, yellow=3, blue=4, magenta=5, cyan=6,
white=7)
# Extended ANSI colors.
# NOTE: each color is provided twice, the second entry is for the
# smaller 8-entry palette. Here they map just exactly back to the
# non-bright variants listed above. This is done so that applications
# can use all the bright variants in a portable manner and the
# application will still behave correctly in low-color environments
# (DOS or Linux Console)
self.add_colors(
bright_black=(8, 0), bright_red=(9, 1), bright_green=(10, 2),
bright_yellow=(11, 3), bright_blue=(12, 4), bright_magenta=(13, 5),
bright_cyan=(14, 6), bright_white=(15, 7))
@property
def active(self):
"""Flag indicating if the color controller is active."""
return self._active
@active.setter
def active(self, value):
"""Set the activity flag."""
self._active = bool(value)
@property
def terminal_palette(self):
"""The currently enabled terminal emulator palette."""
return self._terminal_palette
@terminal_palette.setter
def terminal_palette(self, value):
"""
Set the terminal emulator palette.
:param value:
The slug of a well-known color palette or any
:class:`TerminalPalette` object, the object ``None`` or the string
``"none"``.
:raises ValueError:
If the supplied value is not a well-known palette slug.
:raises TypeError:
If the supplied value unsupported.
"""
if isinstance(value, _string_types):
if value == 'none':
self._terminal_palette = None
return
for palette in self.get_available_terminal_palettes():
if palette.slug == value:
self._terminal_palette = palette
break
else:
raise ValueError("Unknown palette: {!r}".format(value))
elif isinstance(value, TerminalPalette):
self._terminal_palette = value
else:
raise TypeError("value must be a palette or palette slug")
@property
def color_mixer(self):
"""The currently configured color mixer."""
return self._color_mixer
@color_mixer.setter
def color_mixer(self, value):
"""
Set the color mixer.
:param value:
The slug of a well-known color mixer or any :class:`ColorMixerBase`
mixer object, the object ``None`` or the string ``"none"``.
:raises ValueError:
If the supplied value is not a well-known color mixer slug.
:raises TypeError:
If the supplied value unsupported.
"""
if isinstance(value, _string_types):
if value == 'none':
self._color_mixer = None
return
for mixer in self.get_available_color_mixers():
if mixer.slug == value:
self._color_mixer = mixer
break
else:
raise ValueError("Unknown mixer: {!r}".format(value))
elif isinstance(value, ColorMixerBase):
self._color_mixer = value
else:
raise TypeError("value must be a color mixer or color mixer slug")
@property
def accessibility_emulator(self):
"""The currently configured accessibility emulator."""
return self._accessibility_emulator
@accessibility_emulator.setter
def accessibility_emulator(self, value):
"""
Set the accessibility emulator.
:param value:
The slug of a well-known accessibility emulator or any
:class:`AccessibilityEmulator` emulator object, the object ``None``
or the string ``"none"``.
:raises ValueError:
If the supplied value is not a well-known accessibility emulator
slug.
:raises TypeError:
If the supplied value unsupported.
"""
if isinstance(value, _string_types):
if value == 'none':
self._accessibility_emulator = None
return
for emulator in self.get_available_accessibility_emulators():
if emulator.slug == value:
self._accessibility_emulator = emulator
break
else:
raise ValueError("Unknown emulator: {!r}".format(value))
elif isinstance(value, AccessibilityEmulator):
self._accessibility_emulator = value
else:
raise TypeError("value must be an emulator or emulator slug")
def add_colors(self, **palette):
"""
Register additional named colors.
:param palette:
A dictionary with color definitions.
:raises ValueError:
If the palette is malformed.
For a discussion of this method, see :meth:`ColorPalette.add_colors()`.
"""
self._color_palette.add_colors(**palette)
def transform(self, color):
"""
Transform the given color according to configured parameters.
:param color:
Any color understood by guacamole ANSI ingredient.
:returns:
A new color, as understood by the rest of guacamole. This might be
a tuple ``(r, g, b)`` where each item is an integer in range
``0..255``. It might also be an indexed ``0..255`` color.
:raises ValueError:
if the color specification is incorrect
This method can be used by anyone operating with colors. Most notably,
it is used by the ``ansi`` ingredient for color rendering to the
terminal emulator. This method can adjust (change) any of the colors
before it is displayed on the terminal.
.. note::
This method depends on the active flag, pre-configured terminal
palette, pre-configured accessibility emulator and the
pre-configured color mixer.
.. note::
Even if the color controller is disabled it will resolve named
colors from the palette.
"""
if color is None:
return
# Do a palette color-name lookup unconditionally so that named colors
# can be resolved successfully even if the color controller is
# otherwise disabled.
#
# When no mixer is set, the code will prefer indexed-256 mode as it is
# the safest choice.
if self._color_mixer is not None:
preferred_mode = self._color_mixer.preferred_mode
else:
preferred_mode = ColorPalette.PREFER_INDEXED_256
if isinstance(color, _string_types):
color = self._color_palette.resolve(color, preferred_mode)
# If the color controller is not enabled just return the color without
# further transformations.
if not self._active:
return color
# If we don't have a terminal emulator palette or a color mixer then no
# further work should be done, just return an indexed value as-is.
if self._terminal_palette is None or self._color_mixer is None:
return color
# If the color is an indexed color number from the 8 or 256 entry
# palette then use the terminal emulator palette to reconstruct the
# (r, g, b) color.
if isinstance(color, int):
r, g, b = self._terminal_palette.resolve_fast(color)
elif isinstance(color, tuple):
r, g, b = color
else:
raise ValueError(
"Unsupported color representation: {!r}".format(color))
# Allow the emulator to change the color, if one is enabled.
if self._accessibility_emulator is not None:
r, g, b = self._accessibility_emulator.transform(r, g, b)
# Use the mixer to output the final color
return self._color_mixer.mix(r, g, b, self._terminal_palette.palette)
@staticmethod
def get_available_terminal_palettes():
"""
Discover all available terminal emulator color palettes.
:returns:
A tuple containing all supported terminal palettes.
"""
return (
LinuxConsolePalette,
XTermPalette,
GnomeTerminalUbuntu1504Palette,
AppleTerminalOSX1010Palette,
)
@staticmethod
def get_available_accessibility_emulators():
"""
Discover all available accessibility emulators.
:returns:
A tuple containing all supported accessibility emulators.
"""
return (
Normal,
GrayScale1,
GrayScale2,
Protanopia,
Protanomaly,
Deuteranopia,
Deuteranomaly,
Tritanopia,
Tritanomaly,
Achromatopsia,
Achromatomaly,
)
@staticmethod
def get_available_color_mixers():
"""
Discover all available color mixers.
:returns:
A tuple containing all supported color mixers.
"""
return (
TrueColorMixer(),
FastIndexed256ColorMixer(),
FastIndexed8ColorMixer(),
AccurateIndexed256ColorMixer(),
AccurateIndexed8ColorMixer(),
)
class ColorIngredient(Ingredient):
"""Ingredient that exposes control over the color subsystem."""
def __init__(self):
"""Initialize the color ingredient."""
self.color_ctrl = ColorController()
def added(self, context):
"""
Register the color controller in the guacamole execution context.
This method inserts the color controller as the ``color_ctrl``
attribute of the execution context. The ``color:arguments`` spice is
inspected to see if command-line interface specific to the color
controller should be enabled.
"""
context.color_ctrl = self.color_ctrl
self._expose_argparse = context.bowl.has_spice("color:arguments")
self._enable_by_default = context.bowl.has_spice("color:enable")
def build_parser(self, context):
"""
Register color controller arguments in the argument parser.
This method registers the arguments common to the color module in the
argument parser. They are added so that they show up in ``--help``.
"""
if self._expose_argparse:
self._add_argparse_options(context.parser)
def late_init(self, context):
"""
Configure the color controller according to the command-line options.
This method applies the options selected on command line to the color
controller. This includes the active flag, the terminal emulator
palette and the accessibility emulator.
"""
if self._enable_by_default:
context.color_ctrl.active = True
context.color_ctrl.color_mixer = 'fast-indexed-256'
context.color_ctrl.terminal_palette = 'xterm-256color'
if self._expose_argparse:
if context.args.enable_color_controller is True:
self.color_ctrl.active = True
elif context.args.enable_color_controller is False:
self.color_ctrl.active = False
if context.args.terminal_palette:
self.color_ctrl.terminal_palette = (
context.args.terminal_palette)
if context.args.accessibility_emulator:
self.color_ctrl.accessibility_emulator = (
context.args.accessibility_emulator)
if context.args.color_mixer:
self.color_ctrl.color_mixer = context.args.color_mixer
def _add_argparse_options(self, parser):
group = parser.add_argument_group("Color control")
# Add the --enable-color-controller and --disable-color-controller
# arguments. One of those is hidden depending on the presence or
# absence of the 'color:enable' spice.
group.add_argument(
'--enable-color-controller', action='store_true',
default=None, help=(
argparse.SUPPRESS if self._enable_by_default
else _("enable the color controller")))
group.add_argument(
'--disable-color-controller', action='store_false',
dest='enable_color_controller', help=(
argparse.SUPPRESS if not self._enable_by_default
else _("disable the color controller")))
# Add the --terminal-palette argument
group.add_argument(
"--terminal-palette", metavar=_("TERMINAL-PALETTE"),
choices=[str('none')] + [
str(palette.slug) for palette in
self.color_ctrl.get_available_terminal_palettes()],
help="translate indexed colors using selected terminal palette")
# Add the --color-mixer argument
group.add_argument(
"--color-mixer", metavar=_("COLOR-MIXER"),
choices=[str('none')] + [
str(mixer.slug) for mixer in
self.color_ctrl.get_available_color_mixers()],
help="use the specific color mixer")
# Add the --accessibility-emulator argument
group.add_argument(
"--accessibility-emulator", metavar=_("ACCESSIBILITY-EMULATOR"),
choices=[str('none')] + [
str(emulator.slug) for emulator in
self.color_ctrl.get_available_accessibility_emulators()],
help="emulate selected variant of color-blindness")