/ color_picker / wheel_maker.py
wheel_maker.py
  1  # SPDY-FileCopyrightText: 2012 jacksongabbard
  2  # SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
  3  # SPDX-FileCopyrightText: 2021 Kevin Matocha, Jose David M.
  4  #
  5  # SPDX-License-Identifier: MIT
  6  # SPDY-License-Identifier: Unlicense
  7  
  8  """
  9  `wheel_maker`
 10  ================================================================================
 11  
 12  Save a displayio.Bitmap (and associated displayio.Palette) in a BMP file.
 13  This script is adapted in the works from Dave Astels on the ``adafruit_bitmapsaver``
 14  and the works of  Jackson Glabbard
 15  https://jg.gg/2012/05/28/generating-a-color-picker-style-color-wheel-in-python/
 16  https://github.com/jacksongabbard/Python-Color-Gamut-Generator/blob/master/color-wheel-generator.py
 17  and Kevin Matocha on the ``switch_round`` for the ``_color_to_tuple`` function
 18  
 19  * Author(s): Dave Astels, Jackson Glabbard, Kevin Matocha, Jose David M.
 20  
 21  Implementation Notes
 22  --------------------
 23  
 24  
 25  
 26  """
 27  
 28  import math
 29  import struct
 30  import gc
 31  import board
 32  import digitalio
 33  import busio
 34  
 35  try:
 36      import adafruit_sdcard
 37      import storage
 38  except ImportError:
 39      pass
 40  
 41  # pylint: disable=invalid-name, no-member, too-many-locals
 42  # pylint: disable=consider-using-with, duplicate-code
 43  
 44  
 45  def _write_bmp_header(output_file, filesize):
 46      output_file.write(bytes("BM", "ascii"))
 47      output_file.write(struct.pack("<I", filesize))
 48      output_file.write(b"\00\x00")
 49      output_file.write(b"\00\x00")
 50      output_file.write(struct.pack("<I", 54))
 51  
 52  
 53  def _bytes_per_row(source_width):
 54      pixel_bytes = 3 * source_width
 55      padding_bytes = (4 - (pixel_bytes % 4)) % 4
 56      return pixel_bytes + padding_bytes
 57  
 58  
 59  def _write_dib_header(output_file, width, height):
 60      output_file.write(struct.pack("<I", 40))
 61      output_file.write(struct.pack("<I", width))
 62      output_file.write(struct.pack("<I", height))
 63      output_file.write(struct.pack("<H", 1))
 64      output_file.write(struct.pack("<H", 24))
 65      for _ in range(24):
 66          output_file.write(b"\x00")
 67  
 68  
 69  def make_color(base, adj, ratio, shade):
 70      """
 71      Go through each bit of the colors adjusting blue with blue, red with red,
 72      green with green, etc.
 73      """
 74      output = 0x0
 75      bit = 0
 76      for pos in range(3):
 77          base_chan = color_wheel[base][pos]
 78          adj_chan = color_wheel[adj][pos]
 79          new_chan = int(round(base_chan * (1 - ratio) + adj_chan * ratio))
 80  
 81          # now alter the channel by the shade
 82          if shade < 1:
 83              new_chan = new_chan * shade
 84          elif shade > 1:
 85              shade_ratio = shade - 1
 86              new_chan = (0xFF * shade_ratio) + (new_chan * (1 - shade_ratio))
 87  
 88          output = output + (int(new_chan) << bit)
 89          bit = bit + 8
 90      return output
 91  
 92  
 93  def color_to_tuple(value):
 94      """Converts a color from a 24-bit integer to a tuple.
 95      :param value: RGB LED desired value - can be a RGB tuple or a 24-bit integer.
 96      """
 97      if isinstance(value, tuple):
 98          return value
 99      if isinstance(value, int):
100          if value >> 24:
101              raise ValueError("Only bits 0->23 valid for integer input")
102          r = value >> 16
103          g = (value >> 8) & 0xFF
104          b = value & 0xFF
105          return [r, g, b]
106  
107      raise ValueError("Color must be a tuple or 24-bit integer value.")
108  
109  
110  color_wheel = [
111      [0xFF, 0x00, 0xFF],
112      [0xFF, 0x00, 0x00],
113      [0xFF, 0xFF, 0x00],
114      [0x00, 0xFF, 0x00],
115      [0x00, 0xFF, 0xFF],
116      [0x00, 0x00, 0xFF],
117      [0xFF, 0x00, 0xFF],
118  ]
119  
120  
121  def make_wheel(image_name, img_size, bg_color=0x000000):
122      """
123      :param image_name: Name of the ouput bitmap image
124      :param img_size: size of the bitmap image in pixels height=width
125      :param bg_color: color of the background in 24 bit format. Defaults 0x000000
126      :return: color
127  
128  
129      **Quickstart: Importing and using the make_wheel**
130  
131      Here is one way of importing the ``make_wheel`` module so you can use:
132  
133      .. code-block:: python
134  
135          from adafruit_displayio_color_picker import wheel_maker
136  
137      Now you can create a wheel of 200 pixels with a black background using:
138  
139      .. code-block:: python
140  
141          make_wheel("wheel200.bmp", 200 , 0x000000)
142  
143  
144      """
145      img_size_width = img_size
146      img_size_height = img_size
147      img_half = img_size / 2
148      outer_radius = img_size // 2
149      background_color = color_to_tuple(bg_color)
150      row_buffer = bytearray(_bytes_per_row(img_size_width))
151      result_buffer = bytearray(2048)
152  
153      spi = busio.SPI(board.SCK, MOSI=board.MOSI, MISO=board.MISO)
154      cs = digitalio.DigitalInOut(board.SD_CS)
155      sdcard = adafruit_sdcard.SDCard(spi, cs)
156      vfs = storage.VfsFat(sdcard)
157      storage.mount(vfs, "/sd")
158      file_path = "/sd" + image_name
159      print("saving starts")
160      output_file = open(file_path, "wb")
161      filesize = 54 + img_size_height * _bytes_per_row(img_size_width)
162      _write_bmp_header(output_file, filesize)
163      _write_dib_header(output_file, img_size_width, img_size_height)
164  
165      for y in range(img_size, 0, -1):
166          buffer_index = 0
167          for x in range(img_size):
168              dist = abs(math.sqrt((x - img_half) ** 2 + (y - img_half) ** 2))
169              shade = 1 * dist / outer_radius
170              if x - img_half == 0:
171                  angle = -90
172                  if y > img_half:
173                      angle = 90
174              else:
175                  angle = math.atan2((y - img_half), (x - img_half)) * 180 / math.pi
176  
177              angle = (angle - 30) % 360
178  
179              idx = angle / 60
180              if idx < 0:
181                  idx = 6 + idx
182              base = int(round(idx))
183  
184              adj = (6 + base + (-1 if base > idx else 1)) % 6
185              ratio = max(idx, base) - min(idx, base)
186              color = make_color(base, adj, ratio, shade)
187  
188              if dist > outer_radius:
189                  color_rgb = background_color
190              else:
191                  color_rgb = color_to_tuple(color)
192  
193              for b in color_rgb:
194                  row_buffer[buffer_index] = b & 0xFF
195                  buffer_index += 1
196          output_file.write(row_buffer)
197          for i in range(img_size_width * 2):
198              result_buffer[i] = 0
199          gc.collect()
200  
201      output_file.close()
202      print("saving done")