123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313 |
- 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]
|