2
0
Fork 0
textout/textoutpc/color/read.py

258 lines
7.6 KiB
Python
Executable File

#!/usr/bin/env python3
#******************************************************************************
# Copyright (C) 2018 Thomas "Cakeisalie5" Touhey <thomas@touhey.fr>
# This file is part of the textoutpc project, which is MIT-licensed.
#******************************************************************************
""" HTML/CSS like color parsing, mainly for the `[color]` tag.
Defines the `get_color()` function which returns an rgba value.
"""
import re as _re
import math as _math
from .named import colors as _named_colors
from .sys import hls_to_rgb as _hls_to_rgb, hwb_to_rgb as _hwb_to_rgb
__all__ = ["get_color"]
_cr = _re.compile("""
rgba?\s*\(
\s* (?P<rgb_r>[0-9]{1,3}) \s* ([,\\s]
\s* (?P<rgb_g>[0-9]{1,3}) \s* ([,\\s]
\s* (?P<rgb_b>[0-9]{1,3}) \s* ([,\\s]
\s* ((?P<rgb_a_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<rgb_a_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
)?)?)?
\)|
hsla?\s*\(
\s* (?P<hsl_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
(?P<hsl_agl>deg|grad|rad|turn|) \s*[,\\s]
\s* ((?P<hsl_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hsl_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hsl_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hsl_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hsl_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hsl_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*
)?
\)|
hlsa?\s*\(
\s* (?P<hls_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
(?P<hls_agl>deg|grad|rad|turn|) \s*[,\\s]
\s* ((?P<hls_lgt_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hls_lgt_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hls_sat_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hls_sat_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hls_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hls_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)?
\)|
hwb\s*\(
\s* (?P<hwb_hue>-? ([0-9]+\.?|[0-9]*\.[0-9]+) )
(?P<hwb_agl>deg|grad|rad|turn|) \s*[,\\s]
\s* ((?P<hwb_wht_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hwb_wht_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*[,\\s]
\s* ((?P<hwb_blk_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hwb_blk_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*([,\\s/]
\s* ((?P<hwb_aph_per> ([0-9]+\.?|[0-9]*\.[0-9]+) ) \s*%
|(?P<hwb_aph_flt> (0*[01]\.?|0*\.[0-9]+) )) \s*)?
\)|
\# (?P<hex_digits> [0-9a-f]+)
|
(?P<legacy_chars> [0-9a-z]+)
""", _re.VERBOSE | _re.I | _re.M)
def get_color(value):
""" Get a color from a string.
Returns an (r, g, b, a) color.
Raises an exception if the color could not be read. """
# Check if is a color name.
value = value.strip()
try: value = _named_colors[value.lower()]
except: pass
# Initialize the alpha.
alpha = 1.0
# Get the match.
match = _cr.fullmatch(value).groupdict()
if match['hex_digits'] or match['legacy_chars']:
# Imitate the Netscape behaviour. Find more about this here:
# https://stackoverflow.com/a/8333464
#
# I've also extended the thing as I could to introduce more
# modern syntaxes described on the dedicated MDN page:
# https://developer.mozilla.org/en-US/docs/Web/CSS/color_value
#
# First of all, depending on our source, we will act differently:
# - if we are using the `hex_digits` source, then we use the modern
# behaviour and do the fancy things such as `#ABC -> #AABBCC`
# management and possible alpha decoding;
# - if we are using the `legacy_chars` source, then we sanitize our
# input by replacing invalid characters by '0' characters (the
# 0xFFFF limit is due to how UTF-16 was managed at the time).
# We shall also truncate our input to 128 characters.
#
# After these sanitization options, we will keep the same method as
# for legacy color decoding. It should work and be tolerant enough…
members = 3
if match['hex_digits']:
hx = match['hex_digits'].lower()
# RGB and RGBA (3 and 4 char.) notations.
if len(hx) in (3, 4):
hx = hx[0:1] * 2 + hx[1:2] * 2 + hx[2:3] * 2 + hx[3:4] * 2
# Check if there is transparency or not.
if len(hx) % 3 != 0 and len(hx) % 4 == 0:
members = 4
else: # our source is `legacy_chars`
hx = match['legacy_chars'].lower()
hx = ''.join(c if c in '0123456789abcdef' \
else ('0', '00')[ord(c) > 0xFFFF] for c in hx[:128])[:128]
# First, calculate some values we're going to need.
# `iv` is the size of the zone for a member.
# `sz` is the size of the digits slice to take in that zone (max. 8).
# `of` is the offset in the zone of the slice to take.
iv = _math.ceil(len(hx) / members)
of = iv - 8 if iv > 8 else 0
sz = iv - of
# Then isolate the slices using the values calculated above.
# `gr` will be an array of 3 or 4 digit strings (depending on the
# number of members).
gr = list(map(lambda i: hx[i * iv + of:i * iv + iv] \
.ljust(sz, '0'), range(members)))
# Check how many digits we can skip at the beginning of each slice.
pre = min(map(lambda x: len(x) - len(x.lstrip('0')), gr))
pre = min(pre, sz - 2)
# Then extract the values.
it = map(lambda x: int('0' + x[pre:pre + 2], 16), gr)
if members == 3:
r, g, b = it
else:
r, g, b, alpha = it
alpha /= 255.0
elif match['rgb_r']:
# Extract the values.
r = int(match['rgb_r'])
g = int(match['rgb_g']) if match['rgb_g'] else 0
b = int(match['rgb_b']) if match['rgb_b'] else 0
if match['rgb_a_per']:
alpha = float(match['rgb_a_per']) / 100.0
elif match['rgb_a_flt']:
alpha = float(match['rgb_a_flt'])
elif match['hsl_hue'] or match['hls_hue']:
# Extract the values.
if match['hsl_hue']:
hue = float(match['hsl_hue'])
agl = match['hsl_agl']
# Saturation.
if match['hsl_sat_per']:
sat = float(match['hsl_sat_per']) / 100.0
else:
sat = float(match['hsl_sat_flt'])
if sat > 1.0:
sat /= 100.0
# Light.
if match['hsl_lgt_per']:
lgt = float(match['hsl_lgt_per']) / 100.0
else:
lgt = float(match['hsl_lgt_flt'])
if lgt > 1.0:
lgt /= 100.0
# Alpha value.
if match['hsl_aph_per']:
alpha = float(match['hsl_aph_per']) / 100.0
elif match['hsl_aph_flt']:
alpha = float(match['hsl_aph_flt'])
else:
hue = float(match['hls_hue'])
agl = match['hls_agl']
# Saturation.
if match['hls_sat_per']:
sat = float(match['hls_sat_per']) / 100.0
else:
sat = float(match['hls_sat_flt'])
# Light.
if match['hls_lgt_per']:
lgt = float(match['hls_lgt_per']) / 100.0
else:
lgt = float(match['hls_lgt_flt'])
# Alpha value.
if match['hls_aph_per']:
alpha = float(match['hls_aph_per']) / 100.0
elif match['hls_aph_flt']:
alpha = float(match['hls_aph_flt'])
# Prepare the angle.
if agl == 'grad':
hue = hue * 400.0
elif agl == 'rad':
hue = hue / (2 * _math.pi)
elif not agl or agl == 'deg':
hue = hue / 360.0
hue = hue % 1.0
if sat > 1 or lgt > 1:
raise Exception
r, g, b = _hls_to_rgb(hue, lgt, sat)
r, g, b = map(lambda x:int(round(x * 255)), (r, g, b))
elif match['hwb_hue']:
hue = float(match['hwb_hue'])
agl = match['hwb_agl']
# Prepare the angle.
if agl == 'grad':
hue = hue * 400.0
elif agl == 'rad':
hue = hue / (2 * _math.pi)
elif not agl or agl == 'deg':
hue = hue / 360.0
hue = hue % 1.0
# Saturation.
if match['hwb_wht_per']:
wht = float(match['hwb_wht_per']) / 100.0
else:
wht = float(match['hwb_wht_flt'])
# Light.
if match['hwb_blk_per']:
blk = float(match['hwb_blk_per']) / 100.0
else:
blk = float(match['hwb_blk_flt'])
if wht > 1 or blk > 1:
raise Exception
r, g, b = _hwb_to_rgb(hue, wht, blk)
r, g, b = map(lambda x: int(round(x * 255)), (r, g, b))
if r < 0 or r > 255 or g < 0 or g > 255 or b < 0 or b > 255:
raise Exception
if alpha < 0.0 or alpha > 1.0:
raise Exception
alpha = round(alpha, 4)
return (r, g, b, alpha)
# End of file.