/ create_requirement_images.py
create_requirement_images.py
  1  #!/usr/bin/env python3
  2  
  3  """
  4  Create requirement screenshots for learn guide projects
  5  """
  6  
  7  # SPDX-FileCopyrightText: 2021 foamyguy
  8  #
  9  # SPDX-License-Identifier: MIT
 10  
 11  from multiprocessing import Pool
 12  import json
 13  import os
 14  
 15  import click
 16  from PIL import Image, ImageDraw, ImageFont
 17  
 18  from get_imports import (
 19      get_libs_for_project,
 20      get_files_for_project,
 21      get_libs_for_example,
 22      get_files_for_example,
 23      get_learn_guide_cp_projects,
 24  )
 25  
 26  os.makedirs("generated_images", exist_ok=True)
 27  
 28  OUT_WIDTH = 800
 29  PADDING = 20
 30  
 31  INDENT_SIZE = 28
 32  LINE_SPACING = 28
 33  HIGHLIGHT_ROW_COLOR = "#404040"
 34  ROW_COLOR = "#383838"
 35  
 36  TEXT_COLOR = "#B0B0B0"
 37  HIDDEN_TEXT_COLOR = "#808080"
 38  
 39  SHOWN_FILETYPES = ["py", "mpy", "bmp", "pcf", "bdf", "wav", "mp3", "json", "txt"]
 40  
 41  f = open("latest_bundle_data.json", "r")
 42  bundle_data = json.load(f)
 43  f.close()
 44  
 45  
 46  def asset_path(x):
 47      """Return the location of a file shipped with the screenshot maker"""
 48      return os.path.join(os.path.dirname(__file__), x)
 49  
 50  
 51  font = ImageFont.truetype(asset_path("Roboto-Regular.ttf"), 24)
 52  right_triangle = Image.open(asset_path("img/right_triangle.png"))
 53  down_triangle = Image.open(asset_path("img/down_triangle.png"))
 54  
 55  folder_icon = Image.open(asset_path("img/folder.png"))
 56  folder_hidden_icon = Image.open(asset_path("img/folder_hidden.png"))
 57  file_icon = Image.open(asset_path("img/file.png"))
 58  file_hidden_icon = Image.open(asset_path("img/file_hidden.png"))
 59  file_empty_icon = Image.open(asset_path("img/file_empty.png"))
 60  file_empty_hidden_icon = Image.open(asset_path("img/file_empty_hidden.png"))
 61  
 62  file_image_icon = Image.open(asset_path("img/file_image.png"))
 63  file_music_icon = Image.open(asset_path("img/file_music.png"))
 64  file_font_icon = Image.open(asset_path("img/file_font.png"))
 65  
 66  FILE_TYPE_ICON_MAP = {
 67      "py": file_icon,
 68      "mpy": file_icon,
 69      "txt": file_empty_icon,
 70      "bmp": file_image_icon,
 71      "wav": file_music_icon,
 72      "mp3": file_music_icon,
 73      "pcf": file_font_icon,
 74      "bdf": file_font_icon,
 75      "json": file_icon,
 76  }
 77  
 78  # If this is not done, the images fail to load in the subprocesses.
 79  for file_icon in FILE_TYPE_ICON_MAP.values():
 80      file_icon.load()
 81  
 82  
 83  def generate_requirement_image(
 84      project_files, libs, image_name
 85  ):  # pylint: disable=too-many-statements
 86      """Generate a single requirement image"""
 87  
 88      def make_line(
 89          requirement_name, position=(0, 0), icon=None, hidden=False, triangle_icon=None
 90      ):  # pylint: disable=too-many-branches
 91          if triangle_icon:
 92              img.paste(
 93                  triangle_icon,
 94                  (position[0] - 24, position[1] + (LINE_SPACING - 24) // 2),
 95                  mask=triangle_icon,
 96              )
 97          if icon is None:
 98              if requirement_name.endswith(".mpy") or requirement_name.endswith(".py"):
 99                  if hidden:
100                      img.paste(
101                          file_hidden_icon,
102                          (position[0], position[1] + (LINE_SPACING - 24) // 2),
103                          mask=file_hidden_icon,
104                      )
105                  else:
106                      img.paste(
107                          file_icon,
108                          (position[0], position[1] + (LINE_SPACING - 24) // 2),
109                          mask=file_icon,
110                      )
111  
112              elif "." in requirement_name[-5:]:
113                  if hidden:
114                      img.paste(
115                          file_empty_hidden_icon,
116                          (position[0], position[1] + (LINE_SPACING - 24) // 2),
117                          mask=file_empty_icon,
118                      )
119                  else:
120                      img.paste(
121                          file_empty_icon,
122                          (position[0], position[1] + (LINE_SPACING - 24) // 2),
123                          mask=file_empty_icon,
124                      )
125              else:
126                  if hidden:
127                      img.paste(
128                          folder_hidden_icon,
129                          (position[0], position[1] + (LINE_SPACING - 24) // 2),
130                          mask=folder_hidden_icon,
131                      )
132                  else:
133                      img.paste(
134                          folder_icon,
135                          (position[0], position[1] + (LINE_SPACING - 24) // 2),
136                          mask=folder_icon,
137                      )
138          else:
139              img.paste(
140                  icon, (position[0], position[1] + (LINE_SPACING - 24) // 2), mask=icon
141              )
142  
143          if not hidden:
144              draw.text(
145                  (position[0] + 30, position[1] + LINE_SPACING // 2),
146                  requirement_name,
147                  fill=TEXT_COLOR,
148                  anchor="lm",
149                  font=font,
150              )
151          else:
152              draw.text(
153                  (position[0] + 30, position[1] + LINE_SPACING // 2),
154                  requirement_name,
155                  fill=HIDDEN_TEXT_COLOR,
156                  anchor="lm",
157                  font=font,
158              )
159  
160      def make_header(position, project_files):
161          # Static files
162          make_line("CIRCUITPY", position)
163          make_line(
164              ".fseventsd",
165              (position[0] + INDENT_SIZE, position[1] + (LINE_SPACING * 1)),
166              hidden=True,
167              triangle_icon=right_triangle,
168          )
169          make_line(
170              ".metadata_never_index",
171              (position[0] + INDENT_SIZE, position[1] + (LINE_SPACING * 2)),
172              icon=file_empty_hidden_icon,
173              hidden=True,
174          )
175          make_line(
176              ".Trashes",
177              (position[0] + INDENT_SIZE, position[1] + (LINE_SPACING * 3)),
178              icon=file_empty_hidden_icon,
179              hidden=True,
180          )
181          make_line(
182              "boot_out.txt",
183              (position[0] + INDENT_SIZE, position[1] + (LINE_SPACING * 4)),
184          )
185          make_line(
186              "code.py",
187              (position[0] + INDENT_SIZE, position[1] + (LINE_SPACING * 5)),
188              icon=file_icon,
189          )
190  
191          # dynamic files from project dir in learn guide repo
192          rows_added = 0
193          project_files_to_draw = []
194          project_folders_to_draw = []
195          for cur_file in project_files:
196              if "." in cur_file[-5:]:
197                  cur_extension = cur_file.split(".")[-1]
198                  if cur_extension in SHOWN_FILETYPES:
199                      project_files_to_draw.append(cur_file)
200              else:
201                  project_folders_to_draw.append(cur_file)
202  
203          for i, file in enumerate(sorted(project_files_to_draw)):
204              cur_file_extension = file.split(".")[-1]
205  
206              cur_file_icon = FILE_TYPE_ICON_MAP.get(cur_file_extension, file_empty_icon)
207              make_line(
208                  file,
209                  (position[0] + INDENT_SIZE, position[1] + (LINE_SPACING * (6 + i))),
210                  icon=cur_file_icon,
211              )
212              rows_added += 1
213  
214          for i, file in enumerate(sorted(project_folders_to_draw)):
215              make_line(
216                  file,
217                  (
218                      position[0] + INDENT_SIZE,
219                      position[1] + (LINE_SPACING * (6 + i + len(project_files_to_draw))),
220                  ),
221                  triangle_icon=right_triangle,
222              )
223              rows_added += 1
224  
225          make_line(
226              "lib",
227              (
228                  position[0] + INDENT_SIZE,
229                  position[1] + (LINE_SPACING * (5 + rows_added + 1)),
230              ),
231              triangle_icon=down_triangle,
232          )
233  
234      def make_background_highlights(rows, offset=(0, 0)):
235          for i in range(rows):
236              if i % 2 == 0:
237                  draw.rectangle(
238                      [
239                          (0 + offset[0], i * LINE_SPACING + offset[1]),
240                          (OUT_WIDTH - offset[0], (i + 1) * LINE_SPACING + offset[1]),
241                      ],
242                      fill=HIGHLIGHT_ROW_COLOR,
243                  )
244              else:
245                  draw.rectangle(
246                      [
247                          (0 + offset[0], i * LINE_SPACING + offset[1]),
248                          (OUT_WIDTH - offset[0], (i + 1) * LINE_SPACING + offset[1]),
249                      ],
250                      fill=ROW_COLOR,
251                  )
252  
253      def get_dependencies(libraries):
254          package_list = set()
255          file_list = set()
256  
257          libraries_to_check = list(libraries)
258  
259          while len(libraries_to_check) > 0:
260              lib_name = libraries_to_check[0]
261              del libraries_to_check[0]
262  
263              lib_obj = bundle_data[lib_name]
264              for dep_name in lib_obj["dependencies"]:
265                  libraries_to_check.append(dep_name)
266                  dep_obj = bundle_data[dep_name]
267                  if dep_obj["package"]:
268                      package_list.add(dep_name)
269                  else:
270                      file_list.add(dep_name + ".mpy")
271  
272              if lib_obj["package"]:
273                  package_list.add(lib_name)
274              else:
275                  file_list.add(lib_name + ".mpy")
276  
277          return package_list, file_list
278  
279      def sort_libraries(libraries):
280          package_list, file_list = get_dependencies(libraries)
281          return sorted(package_list) + sorted(file_list)
282  
283      def make_libraries(libraries, position):
284  
285          for i, lib_name in enumerate(libraries):
286              triangle_icon = None
287              if not lib_name.endswith(".mpy"):
288                  triangle_icon = right_triangle
289              make_line(
290                  lib_name,
291                  (position[0] + INDENT_SIZE * 2, position[1] + (LINE_SPACING * i)),
292                  triangle_icon=triangle_icon,
293              )
294  
295      final_list_to_render = sort_libraries(libs)
296  
297      if "code.py" in project_files:
298          project_files.remove("code.py")
299  
300      if "main.py" in project_files:
301          project_files.remove("main.py")
302  
303      project_files_count = len(project_files)
304  
305      image_height = (
306          PADDING * 2
307          + 7 * LINE_SPACING
308          + len(final_list_to_render) * LINE_SPACING
309          + (project_files_count) * LINE_SPACING
310      )
311      img = Image.new("RGB", (OUT_WIDTH, image_height), "#303030")
312      draw = ImageDraw.Draw(img)
313  
314      make_background_highlights(
315          7 + len(final_list_to_render) + project_files_count,
316          offset=(PADDING, PADDING),
317      )
318  
319      make_header((PADDING, PADDING), project_files)
320      make_libraries(
321          final_list_to_render,
322          (PADDING, PADDING + (LINE_SPACING * (7 + project_files_count))),
323      )
324  
325      img.save("generated_images/{}.png".format(image_name))
326  
327  
328  def generate_learn_requirement_image(  # pylint: disable=invalid-name
329      learn_guide_project,
330  ):
331      """Generate an image for a single learn project"""
332      image_name = learn_guide_project.replace("/", "_")
333      libs = get_libs_for_project(learn_guide_project)
334      project_files = get_files_for_project(learn_guide_project)
335      generate_requirement_image(project_files, libs, image_name)
336  
337  
338  def generate_example_requirement_image(example_path):  # pylint: disable=invalid-name
339      """Generate an image for a library example"""
340      image_name = "_".join(
341          element
342          for element in example_path.split("/")
343          if element not in ("libraries", "drivers", "helpers", "examples")
344      )
345      libs = get_libs_for_example(example_path)
346      project_files = get_files_for_example(example_path)
347      generate_requirement_image(project_files, libs, image_name)
348  
349  
350  @click.group(invoke_without_command=True)
351  @click.pass_context
352  def cli(ctx):
353      """Main entry point; invokes the learn subcommand if nothing is specified"""
354      if ctx.invoked_subcommand is None:
355          learn()
356  
357  
358  @cli.command()
359  def learn():
360      """Generate images for a learn-style repo"""
361      with Pool() as pool:
362          for _ in pool.imap(
363              generate_learn_requirement_image, get_learn_guide_cp_projects()
364          ):
365              pass
366  
367  
368  @cli.command()
369  @click.argument("paths", nargs=-1)
370  def bundle(paths):
371      """Generate images for a bundle-style repo"""
372      with Pool() as pool:
373          for _ in pool.imap(generate_example_requirement_image, paths):
374              pass
375  
376  
377  if __name__ == "__main__":
378      cli()  # pylint: disable=no-value-for-parameter