/ 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