husl.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. import operator
  2. import math
  3. __version__ = "2.1.0"
  4. m = [
  5. [3.2406, -1.5372, -0.4986],
  6. [-0.9689, 1.8758, 0.0415],
  7. [0.0557, -0.2040, 1.0570]
  8. ]
  9. m_inv = [
  10. [0.4124, 0.3576, 0.1805],
  11. [0.2126, 0.7152, 0.0722],
  12. [0.0193, 0.1192, 0.9505]
  13. ]
  14. # Hard-coded D65 illuminant
  15. refX = 0.95047
  16. refY = 1.00000
  17. refZ = 1.08883
  18. refU = 0.19784
  19. refV = 0.46834
  20. lab_e = 0.008856
  21. lab_k = 903.3
  22. # Public API
  23. def husl_to_rgb(h, s, l):
  24. return lch_to_rgb(*husl_to_lch([h, s, l]))
  25. def husl_to_hex(h, s, l):
  26. return rgb_to_hex(husl_to_rgb(h, s, l))
  27. def rgb_to_husl(r, g, b):
  28. return lch_to_husl(rgb_to_lch(r, g, b))
  29. def hex_to_husl(hex):
  30. return rgb_to_husl(*hex_to_rgb(hex))
  31. def huslp_to_rgb(h, s, l):
  32. return lch_to_rgb(*huslp_to_lch([h, s, l]))
  33. def huslp_to_hex(h, s, l):
  34. return rgb_to_hex(huslp_to_rgb(h, s, l))
  35. def rgb_to_huslp(r, g, b):
  36. return lch_to_huslp(rgb_to_lch(r, g, b))
  37. def hex_to_huslp(hex):
  38. return rgb_to_huslp(*hex_to_rgb(hex))
  39. def lch_to_rgb(l, c, h):
  40. return xyz_to_rgb(luv_to_xyz(lch_to_luv([l, c, h])))
  41. def rgb_to_lch(r, g, b):
  42. return luv_to_lch(xyz_to_luv(rgb_to_xyz([r, g, b])))
  43. def max_chroma(L, H):
  44. hrad = math.radians(H)
  45. sinH = (math.sin(hrad))
  46. cosH = (math.cos(hrad))
  47. sub1 = (math.pow(L + 16, 3.0) / 1560896.0)
  48. sub2 = sub1 if sub1 > 0.008856 else (L / 903.3)
  49. result = float("inf")
  50. for row in m:
  51. m1 = row[0]
  52. m2 = row[1]
  53. m3 = row[2]
  54. top = ((0.99915 * m1 + 1.05122 * m2 + 1.14460 * m3) * sub2)
  55. rbottom = (0.86330 * m3 - 0.17266 * m2)
  56. lbottom = (0.12949 * m3 - 0.38848 * m1)
  57. bottom = (rbottom * sinH + lbottom * cosH) * sub2
  58. for t in (0.0, 1.0):
  59. C = (L * (top - 1.05122 * t) / (bottom + 0.17266 * sinH * t))
  60. if C > 0.0 and C < result:
  61. result = C
  62. return result
  63. def _hrad_extremum(L):
  64. lhs = (math.pow(L, 3.0) + 48.0 * math.pow(L, 2.0) + 768.0 * L + 4096.0) / 1560896.0
  65. rhs = 1107.0 / 125000.0
  66. sub = lhs if lhs > rhs else 10.0 * L / 9033.0
  67. chroma = float("inf")
  68. result = None
  69. for row in m:
  70. for limit in (0.0, 1.0):
  71. [m1, m2, m3] = row
  72. top = -3015466475.0 * m3 * sub + 603093295.0 * m2 * sub - 603093295.0 * limit
  73. bottom = 1356959916.0 * m1 * sub - 452319972.0 * m3 * sub
  74. hrad = math.atan2(top, bottom)
  75. # This is a math hack to deal with tan quadrants, I'm too lazy to figure
  76. # out how to do this properly
  77. if limit == 0.0:
  78. hrad += math.pi
  79. test = max_chroma(L, math.degrees(hrad))
  80. if test < chroma:
  81. chroma = test
  82. result = hrad
  83. return result
  84. def max_chroma_pastel(L):
  85. H = math.degrees(_hrad_extremum(L))
  86. return max_chroma(L, H)
  87. def dot_product(a, b):
  88. return sum(map(operator.mul, a, b))
  89. def f(t):
  90. if t > lab_e:
  91. return (math.pow(t, 1.0 / 3.0))
  92. else:
  93. return (7.787 * t + 16.0 / 116.0)
  94. def f_inv(t):
  95. if math.pow(t, 3.0) > lab_e:
  96. return (math.pow(t, 3.0))
  97. else:
  98. return (116.0 * t - 16.0) / lab_k
  99. def from_linear(c):
  100. if c <= 0.0031308:
  101. return 12.92 * c
  102. else:
  103. return (1.055 * math.pow(c, 1.0 / 2.4) - 0.055)
  104. def to_linear(c):
  105. a = 0.055
  106. if c > 0.04045:
  107. return (math.pow((c + a) / (1.0 + a), 2.4))
  108. else:
  109. return (c / 12.92)
  110. def rgb_prepare(triple):
  111. ret = []
  112. for ch in triple:
  113. ch = round(ch, 3)
  114. if ch < -0.0001 or ch > 1.0001:
  115. raise Exception(f"Illegal RGB value {ch:f}")
  116. if ch < 0:
  117. ch = 0
  118. if ch > 1:
  119. ch = 1
  120. # Fix for Python 3 which by default rounds 4.5 down to 4.0
  121. # instead of Python 2 which is rounded to 5.0 which caused
  122. # a couple off by one errors in the tests. Tests now all pass
  123. # in Python 2 and Python 3
  124. ret.append(int(round(ch * 255 + 0.001, 0)))
  125. return ret
  126. def hex_to_rgb(hex):
  127. if hex.startswith('#'):
  128. hex = hex[1:]
  129. r = int(hex[0:2], 16) / 255.0
  130. g = int(hex[2:4], 16) / 255.0
  131. b = int(hex[4:6], 16) / 255.0
  132. return [r, g, b]
  133. def rgb_to_hex(triple):
  134. [r, g, b] = triple
  135. return '#%02x%02x%02x' % tuple(rgb_prepare([r, g, b]))
  136. def xyz_to_rgb(triple):
  137. xyz = map(lambda row: dot_product(row, triple), m)
  138. return list(map(from_linear, xyz))
  139. def rgb_to_xyz(triple):
  140. rgbl = list(map(to_linear, triple))
  141. return list(map(lambda row: dot_product(row, rgbl), m_inv))
  142. def xyz_to_luv(triple):
  143. X, Y, Z = triple
  144. if X == Y == Z == 0.0:
  145. return [0.0, 0.0, 0.0]
  146. varU = (4.0 * X) / (X + (15.0 * Y) + (3.0 * Z))
  147. varV = (9.0 * Y) / (X + (15.0 * Y) + (3.0 * Z))
  148. L = 116.0 * f(Y / refY) - 16.0
  149. # Black will create a divide-by-zero error
  150. if L == 0.0:
  151. return [0.0, 0.0, 0.0]
  152. U = 13.0 * L * (varU - refU)
  153. V = 13.0 * L * (varV - refV)
  154. return [L, U, V]
  155. def luv_to_xyz(triple):
  156. L, U, V = triple
  157. if L == 0:
  158. return [0.0, 0.0, 0.0]
  159. varY = f_inv((L + 16.0) / 116.0)
  160. varU = U / (13.0 * L) + refU
  161. varV = V / (13.0 * L) + refV
  162. Y = varY * refY
  163. X = 0.0 - (9.0 * Y * varU) / ((varU - 4.0) * varV - varU * varV)
  164. Z = (9.0 * Y - (15.0 * varV * Y) - (varV * X)) / (3.0 * varV)
  165. return [X, Y, Z]
  166. def luv_to_lch(triple):
  167. L, U, V = triple
  168. C = (math.pow(math.pow(U, 2) + math.pow(V, 2), (1.0 / 2.0)))
  169. hrad = (math.atan2(V, U))
  170. H = math.degrees(hrad)
  171. if H < 0.0:
  172. H = 360.0 + H
  173. return [L, C, H]
  174. def lch_to_luv(triple):
  175. L, C, H = triple
  176. Hrad = math.radians(H)
  177. U = (math.cos(Hrad) * C)
  178. V = (math.sin(Hrad) * C)
  179. return [L, U, V]
  180. def husl_to_lch(triple):
  181. H, S, L = triple
  182. if L > 99.9999999:
  183. return [100, 0.0, H]
  184. if L < 0.00000001:
  185. return [0.0, 0.0, H]
  186. mx = max_chroma(L, H)
  187. C = mx / 100.0 * S
  188. return [L, C, H]
  189. def lch_to_husl(triple):
  190. L, C, H = triple
  191. if L > 99.9999999:
  192. return [H, 0.0, 100.0]
  193. if L < 0.00000001:
  194. return [H, 0.0, 0.0]
  195. mx = max_chroma(L, H)
  196. S = C / mx * 100.0
  197. return [H, S, L]
  198. def huslp_to_lch(triple):
  199. H, S, L = triple
  200. if L > 99.9999999:
  201. return [100, 0.0, H]
  202. if L < 0.00000001:
  203. return [0.0, 0.0, H]
  204. mx = max_chroma_pastel(L)
  205. C = mx / 100.0 * S
  206. return [L, C, H]
  207. def lch_to_huslp(triple):
  208. L, C, H = triple
  209. if L > 99.9999999:
  210. return [H, 0.0, 100.0]
  211. if L < 0.00000001:
  212. return [H, 0.0, 0.0]
  213. mx = max_chroma_pastel(L)
  214. S = C / mx * 100.0
  215. return [H, S, L]