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