314 lines
6.5 KiB
Python
314 lines
6.5 KiB
Python
import operator
|
|
import math
|
|
|
|
__version__ = "2.1.0"
|
|
|
|
|
|
m = [
|
|
[3.2406, -1.5372, -0.4986],
|
|
[-0.9689, 1.8758, 0.0415],
|
|
[0.0557, -0.2040, 1.0570]
|
|
]
|
|
|
|
m_inv = [
|
|
[0.4124, 0.3576, 0.1805],
|
|
[0.2126, 0.7152, 0.0722],
|
|
[0.0193, 0.1192, 0.9505]
|
|
]
|
|
|
|
# Hard-coded D65 illuminant
|
|
refX = 0.95047
|
|
refY = 1.00000
|
|
refZ = 1.08883
|
|
refU = 0.19784
|
|
refV = 0.46834
|
|
lab_e = 0.008856
|
|
lab_k = 903.3
|
|
|
|
|
|
# Public API
|
|
|
|
def husl_to_rgb(h, s, l):
|
|
return lch_to_rgb(*husl_to_lch([h, s, l]))
|
|
|
|
|
|
def husl_to_hex(h, s, l):
|
|
return rgb_to_hex(husl_to_rgb(h, s, l))
|
|
|
|
|
|
def rgb_to_husl(r, g, b):
|
|
return lch_to_husl(rgb_to_lch(r, g, b))
|
|
|
|
|
|
def hex_to_husl(hex):
|
|
return rgb_to_husl(*hex_to_rgb(hex))
|
|
|
|
|
|
def huslp_to_rgb(h, s, l):
|
|
return lch_to_rgb(*huslp_to_lch([h, s, l]))
|
|
|
|
|
|
def huslp_to_hex(h, s, l):
|
|
return rgb_to_hex(huslp_to_rgb(h, s, l))
|
|
|
|
|
|
def rgb_to_huslp(r, g, b):
|
|
return lch_to_huslp(rgb_to_lch(r, g, b))
|
|
|
|
|
|
def hex_to_huslp(hex):
|
|
return rgb_to_huslp(*hex_to_rgb(hex))
|
|
|
|
|
|
def lch_to_rgb(l, c, h):
|
|
return xyz_to_rgb(luv_to_xyz(lch_to_luv([l, c, h])))
|
|
|
|
|
|
def rgb_to_lch(r, g, b):
|
|
return luv_to_lch(xyz_to_luv(rgb_to_xyz([r, g, b])))
|
|
|
|
|
|
def max_chroma(L, H):
|
|
hrad = math.radians(H)
|
|
sinH = (math.sin(hrad))
|
|
cosH = (math.cos(hrad))
|
|
sub1 = (math.pow(L + 16, 3.0) / 1560896.0)
|
|
sub2 = sub1 if sub1 > 0.008856 else (L / 903.3)
|
|
result = float("inf")
|
|
for row in m:
|
|
m1 = row[0]
|
|
m2 = row[1]
|
|
m3 = row[2]
|
|
top = ((0.99915 * m1 + 1.05122 * m2 + 1.14460 * m3) * sub2)
|
|
rbottom = (0.86330 * m3 - 0.17266 * m2)
|
|
lbottom = (0.12949 * m3 - 0.38848 * m1)
|
|
bottom = (rbottom * sinH + lbottom * cosH) * sub2
|
|
|
|
for t in (0.0, 1.0):
|
|
C = (L * (top - 1.05122 * t) / (bottom + 0.17266 * sinH * t))
|
|
if C > 0.0 and C < result:
|
|
result = C
|
|
return result
|
|
|
|
|
|
def _hrad_extremum(L):
|
|
lhs = (math.pow(L, 3.0) + 48.0 * math.pow(L, 2.0) + 768.0 * L + 4096.0) / 1560896.0
|
|
rhs = 1107.0 / 125000.0
|
|
sub = lhs if lhs > rhs else 10.0 * L / 9033.0
|
|
chroma = float("inf")
|
|
result = None
|
|
for row in m:
|
|
for limit in (0.0, 1.0):
|
|
[m1, m2, m3] = row
|
|
top = -3015466475.0 * m3 * sub + 603093295.0 * m2 * sub - 603093295.0 * limit
|
|
bottom = 1356959916.0 * m1 * sub - 452319972.0 * m3 * sub
|
|
hrad = math.atan2(top, bottom)
|
|
# This is a math hack to deal with tan quadrants, I'm too lazy to figure
|
|
# out how to do this properly
|
|
if limit == 0.0:
|
|
hrad += math.pi
|
|
test = max_chroma(L, math.degrees(hrad))
|
|
if test < chroma:
|
|
chroma = test
|
|
result = hrad
|
|
return result
|
|
|
|
|
|
def max_chroma_pastel(L):
|
|
H = math.degrees(_hrad_extremum(L))
|
|
return max_chroma(L, H)
|
|
|
|
|
|
def dot_product(a, b):
|
|
return sum(map(operator.mul, a, b))
|
|
|
|
|
|
def f(t):
|
|
if t > lab_e:
|
|
return (math.pow(t, 1.0 / 3.0))
|
|
else:
|
|
return (7.787 * t + 16.0 / 116.0)
|
|
|
|
|
|
def f_inv(t):
|
|
if math.pow(t, 3.0) > lab_e:
|
|
return (math.pow(t, 3.0))
|
|
else:
|
|
return (116.0 * t - 16.0) / lab_k
|
|
|
|
|
|
def from_linear(c):
|
|
if c <= 0.0031308:
|
|
return 12.92 * c
|
|
else:
|
|
return (1.055 * math.pow(c, 1.0 / 2.4) - 0.055)
|
|
|
|
|
|
def to_linear(c):
|
|
a = 0.055
|
|
|
|
if c > 0.04045:
|
|
return (math.pow((c + a) / (1.0 + a), 2.4))
|
|
else:
|
|
return (c / 12.92)
|
|
|
|
|
|
def rgb_prepare(triple):
|
|
ret = []
|
|
for ch in triple:
|
|
ch = round(ch, 3)
|
|
|
|
if ch < -0.0001 or ch > 1.0001:
|
|
raise Exception(f"Illegal RGB value {ch:f}")
|
|
|
|
if ch < 0:
|
|
ch = 0
|
|
if ch > 1:
|
|
ch = 1
|
|
|
|
# Fix for Python 3 which by default rounds 4.5 down to 4.0
|
|
# instead of Python 2 which is rounded to 5.0 which caused
|
|
# a couple off by one errors in the tests. Tests now all pass
|
|
# in Python 2 and Python 3
|
|
ret.append(int(round(ch * 255 + 0.001, 0)))
|
|
|
|
return ret
|
|
|
|
|
|
def hex_to_rgb(hex):
|
|
if hex.startswith('#'):
|
|
hex = hex[1:]
|
|
r = int(hex[0:2], 16) / 255.0
|
|
g = int(hex[2:4], 16) / 255.0
|
|
b = int(hex[4:6], 16) / 255.0
|
|
return [r, g, b]
|
|
|
|
|
|
def rgb_to_hex(triple):
|
|
[r, g, b] = triple
|
|
return '#%02x%02x%02x' % tuple(rgb_prepare([r, g, b]))
|
|
|
|
|
|
def xyz_to_rgb(triple):
|
|
xyz = map(lambda row: dot_product(row, triple), m)
|
|
return list(map(from_linear, xyz))
|
|
|
|
|
|
def rgb_to_xyz(triple):
|
|
rgbl = list(map(to_linear, triple))
|
|
return list(map(lambda row: dot_product(row, rgbl), m_inv))
|
|
|
|
|
|
def xyz_to_luv(triple):
|
|
X, Y, Z = triple
|
|
|
|
if X == Y == Z == 0.0:
|
|
return [0.0, 0.0, 0.0]
|
|
|
|
varU = (4.0 * X) / (X + (15.0 * Y) + (3.0 * Z))
|
|
varV = (9.0 * Y) / (X + (15.0 * Y) + (3.0 * Z))
|
|
L = 116.0 * f(Y / refY) - 16.0
|
|
|
|
# Black will create a divide-by-zero error
|
|
if L == 0.0:
|
|
return [0.0, 0.0, 0.0]
|
|
|
|
U = 13.0 * L * (varU - refU)
|
|
V = 13.0 * L * (varV - refV)
|
|
|
|
return [L, U, V]
|
|
|
|
|
|
def luv_to_xyz(triple):
|
|
L, U, V = triple
|
|
|
|
if L == 0:
|
|
return [0.0, 0.0, 0.0]
|
|
|
|
varY = f_inv((L + 16.0) / 116.0)
|
|
varU = U / (13.0 * L) + refU
|
|
varV = V / (13.0 * L) + refV
|
|
Y = varY * refY
|
|
X = 0.0 - (9.0 * Y * varU) / ((varU - 4.0) * varV - varU * varV)
|
|
Z = (9.0 * Y - (15.0 * varV * Y) - (varV * X)) / (3.0 * varV)
|
|
|
|
return [X, Y, Z]
|
|
|
|
|
|
def luv_to_lch(triple):
|
|
L, U, V = triple
|
|
|
|
C = (math.pow(math.pow(U, 2) + math.pow(V, 2), (1.0 / 2.0)))
|
|
hrad = (math.atan2(V, U))
|
|
H = math.degrees(hrad)
|
|
if H < 0.0:
|
|
H = 360.0 + H
|
|
|
|
return [L, C, H]
|
|
|
|
|
|
def lch_to_luv(triple):
|
|
L, C, H = triple
|
|
|
|
Hrad = math.radians(H)
|
|
U = (math.cos(Hrad) * C)
|
|
V = (math.sin(Hrad) * C)
|
|
|
|
return [L, U, V]
|
|
|
|
|
|
def husl_to_lch(triple):
|
|
H, S, L = triple
|
|
|
|
if L > 99.9999999:
|
|
return [100, 0.0, H]
|
|
if L < 0.00000001:
|
|
return [0.0, 0.0, H]
|
|
|
|
mx = max_chroma(L, H)
|
|
C = mx / 100.0 * S
|
|
|
|
return [L, C, H]
|
|
|
|
|
|
def lch_to_husl(triple):
|
|
L, C, H = triple
|
|
|
|
if L > 99.9999999:
|
|
return [H, 0.0, 100.0]
|
|
if L < 0.00000001:
|
|
return [H, 0.0, 0.0]
|
|
|
|
mx = max_chroma(L, H)
|
|
S = C / mx * 100.0
|
|
|
|
return [H, S, L]
|
|
|
|
|
|
def huslp_to_lch(triple):
|
|
H, S, L = triple
|
|
|
|
if L > 99.9999999:
|
|
return [100, 0.0, H]
|
|
if L < 0.00000001:
|
|
return [0.0, 0.0, H]
|
|
|
|
mx = max_chroma_pastel(L)
|
|
C = mx / 100.0 * S
|
|
|
|
return [L, C, H]
|
|
|
|
|
|
def lch_to_huslp(triple):
|
|
L, C, H = triple
|
|
|
|
if L > 99.9999999:
|
|
return [H, 0.0, 100.0]
|
|
if L < 0.00000001:
|
|
return [H, 0.0, 0.0]
|
|
|
|
mx = max_chroma_pastel(L)
|
|
S = C / mx * 100.0
|
|
|
|
return [H, S, L]
|