diff options
author | Nick Brassel <nick@tzarc.org> | 2024-02-28 21:47:37 +1100 |
---|---|---|
committer | Nick Brassel <nick@tzarc.org> | 2024-02-28 21:47:37 +1100 |
commit | 4e369d405af6bba1adce6337b2e1b1ea1788566c (patch) | |
tree | b0f020feff1809e37c9e7795d344929ff0bb290a /lib/python/qmk | |
parent | 4e04da397ef643f8fcf4afbe1d19f63aee1fc561 (diff) | |
parent | dd1706e468bb18dd7f7ae143de735a5d3be1bfb8 (diff) |
Merge branch 'develop'
Diffstat (limited to 'lib/python/qmk')
-rw-r--r-- | lib/python/qmk/build_targets.py | 5 | ||||
-rwxr-xr-x | lib/python/qmk/cli/bux.py | 4 | ||||
-rw-r--r-- | lib/python/qmk/cli/find.py | 4 | ||||
-rwxr-xr-x | lib/python/qmk/cli/format/json.py | 4 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/compilation_database.py | 4 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/config_h.py | 2 | ||||
-rwxr-xr-x | lib/python/qmk/cli/generate/keyboard_c.py | 25 | ||||
-rw-r--r-- | lib/python/qmk/cli/git/submodule.py | 8 | ||||
-rw-r--r-- | lib/python/qmk/cli/lint.py | 15 | ||||
-rwxr-xr-x | lib/python/qmk/cli/mass_compile.py | 6 | ||||
-rw-r--r-- | lib/python/qmk/cli/migrate.py | 5 | ||||
-rw-r--r-- | lib/python/qmk/info.py | 46 | ||||
-rw-r--r-- | lib/python/qmk/painter_qgf.py | 304 | ||||
-rw-r--r-- | lib/python/qmk/search.py | 111 |
14 files changed, 335 insertions, 208 deletions
diff --git a/lib/python/qmk/build_targets.py b/lib/python/qmk/build_targets.py index 80f587bcc0..d974d04020 100644 --- a/lib/python/qmk/build_targets.py +++ b/lib/python/qmk/build_targets.py @@ -119,9 +119,10 @@ class BuildTarget: command = self.compile_command(build_target=build_target, dry_run=True, **env_vars) from qmk.cli.generate.compilation_database import write_compilation_database # Lazy load due to circular references output_path = QMK_FIRMWARE / 'compile_commands.json' - write_compilation_database(command=command, output_path=output_path, skip_clean=skip_clean, **env_vars) - if output_path.exists() and HAS_QMK_USERSPACE: + ret = write_compilation_database(command=command, output_path=output_path, skip_clean=skip_clean, **env_vars) + if ret and output_path.exists() and HAS_QMK_USERSPACE: shutil.copy(str(output_path), str(QMK_USERSPACE / 'compile_commands.json')) + return ret def compile(self, build_target: str = None, dry_run: bool = False, **env_vars) -> None: if self._clean or self._compiledb: diff --git a/lib/python/qmk/cli/bux.py b/lib/python/qmk/cli/bux.py index 8c7f172779..669521d08e 100755 --- a/lib/python/qmk/cli/bux.py +++ b/lib/python/qmk/cli/bux.py @@ -19,7 +19,7 @@ def bux(cli): config.set_config('user', 'bux', bux + 1) cli.save_config() - buck = """ + buck = r""" @@BBBBBBBBBBBBBBBBBBBBK `vP8#####BE2~ x###g_ `S###q n##} -j#Bl. vBBBBBBBBBBBBBBBBBBBB@@ @B `:!: ^#@#]- `!t@@&. 7@@B@#^ _Q@Q@@R y@@l:P@#1' `!!_ B@ @B r@@@B g@@| ` N@@u 7@@iv@@u *#@z"@@R y@@&@@Q- l@@@D B@ @@ -34,7 +34,7 @@ def bux(cli): @B _y ]# ,c vUWNWWPsfsssN9WyccnckAfUfWb0DR0&R5RRRddq2_ `@D`jr@2U@#c3@1@Qc- B@ @B !7! .r]` }AE0RdRqNd9dNR9fUIzzosPqqAddNNdER9EE9dPy! BQ!zy@iU@.Q@@y@8x- B@ @B :****>. '7adddDdR&gRNdRbd&dNNbbRdNdd5NdRRD0RSf}- .k0&EW`xR .8Q=NRRx B@ -@B =**-rx*r}r~}" ;n2jkzsf3N3zsKsP5dddRddddRddNNqPzy\\" '~****" B@ +@B =**-rx*r}r~}" ;n2jkzsf3N3zsKsP5dddRddddRddNNqPzy\" '~****" B@ @B :!!~!;=~r>:*_ `:^vxikylulKfHkyjzzozoIoklix|^!-` B@ @B ```'-_""::::!:_-.`` B@ @B `- .` B@ diff --git a/lib/python/qmk/cli/find.py b/lib/python/qmk/cli/find.py index 55a0530092..8f3a29c90c 100644 --- a/lib/python/qmk/cli/find.py +++ b/lib/python/qmk/cli/find.py @@ -1,7 +1,7 @@ """Command to search through all keyboards and keymaps for a given search criteria. """ from milc import cli -from qmk.search import search_keymap_targets +from qmk.search import filter_help, search_keymap_targets @cli.argument( @@ -11,7 +11,7 @@ from qmk.search import search_keymap_targets action='append', default=[], help= # noqa: `format-python` and `pytest` don't agree here. - "Filter the list of keyboards based on their info.json data. Accepts the formats key=value, function(key), or function(key,value), eg. 'features.rgblight=true'. Valid functions are 'absent', 'contains', 'exists' and 'length'. May be passed multiple times; all filters need to match. Value may include wildcards such as '*' and '?'." # noqa: `format-python` and `pytest` don't agree here. + f"Filter the list of keyboards based on their info.json data. Accepts the formats key=value, function(key), or function(key,value), eg. 'features.rgblight=true'. Valid functions are {filter_help()}. May be passed multiple times; all filters need to match. Value may include wildcards such as '*' and '?'." # noqa: `format-python` and `pytest` don't agree here. ) @cli.argument('-p', '--print', arg_only=True, action='append', default=[], help="For each matched target, print the value of the supplied info.json key. May be passed multiple times.") @cli.argument('-km', '--keymap', type=str, default='default', help="The keymap name to build. Default is 'default'.") diff --git a/lib/python/qmk/cli/format/json.py b/lib/python/qmk/cli/format/json.py index 283513254c..87a3837d10 100755 --- a/lib/python/qmk/cli/format/json.py +++ b/lib/python/qmk/cli/format/json.py @@ -92,8 +92,8 @@ def format_json(cli): output = json.dumps(json_data, cls=json_encoder, sort_keys=True) if cli.args.inplace: - with open(cli.args.json_file, 'w+', encoding='utf-8') as outfile: - outfile.write(output) + with open(cli.args.json_file, 'w+', encoding='utf-8', newline='\n') as outfile: + outfile.write(output + '\n') # Display the results if print was set # We don't operate in-place by default, so also display to stdout diff --git a/lib/python/qmk/cli/generate/compilation_database.py b/lib/python/qmk/cli/generate/compilation_database.py index 5100d2b6d2..a2190fee66 100755 --- a/lib/python/qmk/cli/generate/compilation_database.py +++ b/lib/python/qmk/cli/generate/compilation_database.py @@ -17,6 +17,7 @@ from qmk.constants import QMK_FIRMWARE from qmk.decorators import automagic_keyboard, automagic_keymap from qmk.keyboard import keyboard_completer, keyboard_folder from qmk.keymap import keymap_completer +from qmk.build_targets import KeyboardKeymapBuildTarget @lru_cache(maxsize=10) @@ -138,4 +139,5 @@ def generate_compilation_database(cli: MILC) -> Union[bool, int]: elif not current_keymap: cli.log.error('Could not determine keymap!') - return write_compilation_database(current_keyboard, current_keymap, QMK_FIRMWARE / 'compile_commands.json') + target = KeyboardKeymapBuildTarget(current_keyboard, current_keymap) + return target.generate_compilation_database() diff --git a/lib/python/qmk/cli/generate/config_h.py b/lib/python/qmk/cli/generate/config_h.py index 00fb1d9585..fc681300a3 100755 --- a/lib/python/qmk/cli/generate/config_h.py +++ b/lib/python/qmk/cli/generate/config_h.py @@ -108,6 +108,8 @@ def generate_config_items(kb_info_json, config_h_lines): elif key_type.startswith('array'): config_h_lines.append(generate_define(config_key, f'{{ {", ".join(map(str, config_value))} }}')) elif key_type == 'bool': + config_h_lines.append(generate_define(config_key, 'true' if config_value else 'false')) + elif key_type == 'flag': if config_value: config_h_lines.append(generate_define(config_key)) elif key_type == 'mapping': diff --git a/lib/python/qmk/cli/generate/keyboard_c.py b/lib/python/qmk/cli/generate/keyboard_c.py index f010341613..5a6c967486 100755 --- a/lib/python/qmk/cli/generate/keyboard_c.py +++ b/lib/python/qmk/cli/generate/keyboard_c.py @@ -9,21 +9,25 @@ from qmk.path import normpath from qmk.constants import GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE -def _gen_led_config(info_data): +def _gen_led_configs(info_data): + lines = [] + + if 'layout' in info_data.get('rgb_matrix', {}): + lines.extend(_gen_led_config(info_data, 'rgb_matrix')) + + if 'layout' in info_data.get('led_matrix', {}): + lines.extend(_gen_led_config(info_data, 'led_matrix')) + + return lines + + +def _gen_led_config(info_data, config_type): """Convert info.json content to g_led_config """ cols = info_data['matrix_size']['cols'] rows = info_data['matrix_size']['rows'] - config_type = None - if 'layout' in info_data.get('rgb_matrix', {}): - config_type = 'rgb_matrix' - elif 'layout' in info_data.get('led_matrix', {}): - config_type = 'led_matrix' - lines = [] - if not config_type: - return lines matrix = [['NO_LED'] * cols for _ in range(rows)] pos = [] @@ -53,6 +57,7 @@ def _gen_led_config(info_data): lines.append(f' {{ {", ".join(flags)} }},') lines.append('};') lines.append('#endif') + lines.append('') return lines @@ -98,7 +103,7 @@ def generate_keyboard_c(cli): # Build the layouts.h file. keyboard_h_lines = [GPL2_HEADER_C_LIKE, GENERATED_HEADER_C_LIKE, '#include QMK_KEYBOARD_H', ''] - keyboard_h_lines.extend(_gen_led_config(kb_info_json)) + keyboard_h_lines.extend(_gen_led_configs(kb_info_json)) keyboard_h_lines.extend(_gen_matrix_mask(kb_info_json)) # Show the results diff --git a/lib/python/qmk/cli/git/submodule.py b/lib/python/qmk/cli/git/submodule.py index ef116ea124..1cbfd74e88 100644 --- a/lib/python/qmk/cli/git/submodule.py +++ b/lib/python/qmk/cli/git/submodule.py @@ -1,8 +1,8 @@ import shutil +from pathlib import Path from milc import cli -from qmk.path import normpath from qmk import submodules REMOVE_DIRS = [ @@ -40,12 +40,12 @@ def git_submodule(cli): remove_dirs = REMOVE_DIRS if cli.config.git_submodule.force: # Also trash everything that isnt marked as "safe" - for path in normpath('lib').iterdir(): + for path in Path('lib').iterdir(): if not any(ignore in path.as_posix() for ignore in IGNORE_DIRS): remove_dirs.append(path) - for folder in map(normpath, remove_dirs): - if normpath(folder).is_dir(): + for folder in map(Path, remove_dirs): + if folder.is_dir(): print(f"Removing '{folder}'") shutil.rmtree(folder) diff --git a/lib/python/qmk/cli/lint.py b/lib/python/qmk/cli/lint.py index a7c85b5643..7ebb0cf9c4 100644 --- a/lib/python/qmk/cli/lint.py +++ b/lib/python/qmk/cli/lint.py @@ -13,6 +13,7 @@ from qmk.git import git_get_ignored_files from qmk.c_parse import c_source_files CHIBIOS_CONF_CHECKS = ['chconf.h', 'halconf.h', 'mcuconf.h', 'board.h'] +INVALID_KB_FEATURES = set(['encoder_map', 'dip_switch_map', 'combo', 'tap_dance', 'via']) def _list_defaultish_keymaps(kb): @@ -69,6 +70,17 @@ def _handle_json_errors(kb, info): return ok +def _handle_invalid_features(kb, info): + """Check for features that should never be enabled at the keyboard level + """ + ok = True + features = set(info.get('features', [])) + for found in features & INVALID_KB_FEATURES: + ok = False + cli.log.error(f'{kb}: Invalid keyboard level feature detected - {found}') + return ok + + def _chibios_conf_includenext_check(target): """Check the ChibiOS conf.h for the correct inclusion of the next conf.h """ @@ -154,6 +166,9 @@ def keyboard_check(kb): ok = False # Additional checks + if not _handle_invalid_features(kb, kb_info): + ok = False + rules_mk_assignment_errors = _rules_mk_assignment_only(kb) if rules_mk_assignment_errors: ok = False diff --git a/lib/python/qmk/cli/mass_compile.py b/lib/python/qmk/cli/mass_compile.py index 69b9103fdc..7db704d6c2 100755 --- a/lib/python/qmk/cli/mass_compile.py +++ b/lib/python/qmk/cli/mass_compile.py @@ -52,9 +52,9 @@ all: {keyboard_safe}_{keymap_name}_binary {' '.join(command)} \\ >>"{build_log}" 2>&1 \\ || cp "{build_log}" "{failed_log}" - @{{ grep '\[ERRORS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;31m[ERRORS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\ - || {{ grep '\[WARNINGS\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \e[1;33m[WARNINGS]\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\ - || printf "Build %-64s \e[1;32m[OK]\e[0m\\n" "{keyboard_name}:{keymap_name}" + @{{ grep '\\[ERRORS\\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \\e[1;31m[ERRORS]\\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\ + || {{ grep '\\[WARNINGS\\]' "{build_log}" >/dev/null 2>&1 && printf "Build %-64s \\e[1;33m[WARNINGS]\\e[0m\\n" "{keyboard_name}:{keymap_name}" ; }} \\ + || printf "Build %-64s \\e[1;32m[OK]\\e[0m\\n" "{keyboard_name}:{keymap_name}" @rm -f "{build_log}" || true """# noqa ) diff --git a/lib/python/qmk/cli/migrate.py b/lib/python/qmk/cli/migrate.py index c1b1ad1ea9..0bab5c1949 100644 --- a/lib/python/qmk/cli/migrate.py +++ b/lib/python/qmk/cli/migrate.py @@ -47,9 +47,12 @@ def migrate(cli): files = _candidate_files(cli.args.keyboard) # Filter down keys if requested - keys = info_map.keys() + keys = list(filter(lambda key: info_map[key].get("to_json", True), info_map.keys())) if cli.args.filter: keys = list(set(keys) & set(cli.args.filter)) + rejected = set(cli.args.filter) - set(keys) + for key in rejected: + cli.log.info(f'{{fg_yellow}}Skipping {key} as migration not possible...') cli.log.info(f'{{fg_green}}Migrating keyboard {{fg_cyan}}{cli.args.keyboard}{{fg_green}}.{{fg_reset}}') diff --git a/lib/python/qmk/info.py b/lib/python/qmk/info.py index d42ba5c660..13588abdb8 100644 --- a/lib/python/qmk/info.py +++ b/lib/python/qmk/info.py @@ -233,6 +233,9 @@ def _extract_features(info_data, rules): key = '_'.join(key.split('_')[:-1]).lower() value = True if value.lower() in true_values else False if value.lower() in false_values else value + if key in ['lto']: + continue + if 'config_h_features' not in info_data: info_data['config_h_features'] = {} @@ -524,6 +527,9 @@ def _config_to_json(key_type, config_value): """Convert config value using spec """ if key_type.startswith('array'): + if key_type.count('.') > 1: + raise Exception(f"Conversion of {key_type} not possible") + if '.' in key_type: key_type, array_type = key_type.split('.', 1) else: @@ -536,7 +542,7 @@ def _config_to_json(key_type, config_value): else: return list(map(str.strip, config_value.split(','))) - elif key_type == 'bool': + elif key_type in ['bool', 'flag']: if isinstance(config_value, bool): return config_value return config_value in true_values @@ -706,27 +712,23 @@ def _extract_led_config(info_data, keyboard): cols = info_data['matrix_size']['cols'] rows = info_data['matrix_size']['rows'] - # Determine what feature owns g_led_config - features = info_data.get("features", {}) - feature = None - if features.get("rgb_matrix", False): - feature = "rgb_matrix" - elif features.get("led_matrix", False): - feature = "led_matrix" - - if feature: - # Process - for file in find_keyboard_c(keyboard): - try: - ret = find_led_config(file, cols, rows) - if ret: - info_data[feature] = info_data.get(feature, {}) - info_data[feature]["layout"] = ret - except Exception as e: - _log_warning(info_data, f'led_config: {file.name}: {e}') - - if info_data[feature].get("layout", None) and not info_data[feature].get("led_count", None): - info_data[feature]["led_count"] = len(info_data[feature]["layout"]) + for feature in ['rgb_matrix', 'led_matrix']: + if info_data.get('features', {}).get(feature, False) or feature in info_data: + + # Only attempt search if dd led config is missing + if 'layout' not in info_data.get(feature, {}): + # Process + for file in find_keyboard_c(keyboard): + try: + ret = find_led_config(file, cols, rows) + if ret: + info_data[feature] = info_data.get(feature, {}) + info_data[feature]['layout'] = ret + except Exception as e: + _log_warning(info_data, f'led_config: {file.name}: {e}') + + if info_data[feature].get('layout', None) and not info_data[feature].get('led_count', None): + info_data[feature]['led_count'] = len(info_data[feature]['layout']) return info_data diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py index 2b8edfb04d..cc4697f1c6 100644 --- a/lib/python/qmk/painter_qgf.py +++ b/lib/python/qmk/painter_qgf.py @@ -1,9 +1,11 @@ # Copyright 2021 Nick Brassel (@tzarc) +# Copyright 2023 Pablo Martinez (@elpekenin) <elpekenin@elpekenin.dev> # SPDX-License-Identifier: GPL-2.0-or-later # Quantum Graphics File "QGF" Image File Format. # See https://docs.qmk.fm/#/quantum_painter_qgf for more information. +import functools from colorsys import rgb_to_hsv from types import FunctionType from PIL import Image, ImageFile, ImageChops @@ -15,6 +17,12 @@ def o24(i): return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16) +# Helper to convert from RGB888 to the QMK "dialect" of HSV888 +def rgb888_to_qmk_hsv888(e): + hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0) + return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0)) + + ######################################################################################################################## @@ -60,6 +68,14 @@ class QGFGraphicsDescriptor: + o16(self.frame_count) # frame count ) + @property + def image_size(self): + return self.image_width, self.image_height + + @image_size.setter + def image_size(self, size): + self.image_width, self.image_height = size + ######################################################################################################################## @@ -180,6 +196,14 @@ class QGFFrameDeltaDescriptorV1: + o16(self.bottom) # bottom ) + @property + def bbox(self): + return self.left, self.top, self.right, self.bottom + + @bbox.setter + def bbox(self, bbox): + self.left, self.top, self.right, self.bottom = bbox + ######################################################################################################################## @@ -221,42 +245,159 @@ def _accept(prefix): return False -def _save(im, fp, filename): +def _for_all_frames(x: FunctionType, /, images): + frame_num = 0 + last_frame = None + for frame in images: + # Get number of of frames in this image + nfr = getattr(frame, "n_frames", 1) + for idx in range(nfr): + frame.seek(idx) + frame.load() + copy = frame.copy().convert("RGB") + x(frame_num, copy, last_frame) + last_frame = copy + frame_num += 1 + + +def _compress_image(frame, last_frame, *, use_rle, use_deltas, format_, **_kwargs): + # Convert the original frame so we can do comparisons + converted = qmk.painter.convert_requested_format(frame, format_) + graphic_data = qmk.painter.convert_image_bytes(converted, format_) + + # Convert the raw data to RLE-encoded if requested + raw_data = graphic_data[1] + if use_rle: + rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1]) + use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data) + image_data = raw_data if use_raw_this_frame else rle_data + + # Work out if a delta frame is smaller than injecting it directly + use_delta_this_frame = False + bbox = None + if use_deltas and last_frame is not None: + # If we want to use deltas, then find the difference + diff = ImageChops.difference(frame, last_frame) + + # Get the bounding box of those differences + bbox = diff.getbbox() + + # If we have a valid bounding box... + if bbox: + # ...create the delta frame by cropping the original. + delta_frame = frame.crop(bbox) + + # Convert the delta frame to the requested format + delta_converted = qmk.painter.convert_requested_format(delta_frame, format_) + delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format_) + + # Work out how large the delta frame is going to be with compression etc. + delta_raw_data = delta_graphic_data[1] + if use_rle: + delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1]) + delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data) + delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data + + # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead + # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash + # sizing constraints. + if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data): + # Copy across all the delta equivalents so that the rest of the processing acts on those + graphic_data = delta_graphic_data + raw_data = delta_raw_data + rle_data = delta_rle_data + use_raw_this_frame = delta_use_raw_this_frame + image_data = delta_image_data + use_delta_this_frame = True + + # Default to whole image + bbox = bbox or [0, 0, *frame.size] + # Fix sze (as per #20296), we need to cast first as tuples are inmutable + bbox = list(bbox) + bbox[2] -= 1 + bbox[3] -= 1 + + return { + "bbox": bbox, + "graphic_data": graphic_data, + "image_data": image_data, + "use_delta_this_frame": use_delta_this_frame, + "use_raw_this_frame": use_raw_this_frame, + } + + +# Helper function to save each frame to the output file +def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, **kwargs): + # Not an argument of the function as it would consume from **kwargs + format_ = kwargs["format_"] + + # (potentially) Apply RLE and/or delta, and work out output image's information + outputs = _compress_image(frame, last_frame, **kwargs) + bbox = outputs["bbox"] + graphic_data = outputs["graphic_data"] + image_data = outputs["image_data"] + use_delta_this_frame = outputs["use_delta_this_frame"] + use_raw_this_frame = outputs["use_raw_this_frame"] + + # Write out the frame descriptor + frame_offsets.frame_offsets[idx] = fp.tell() + vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h') + frame_descriptor = QGFFrameDescriptorV1() + frame_descriptor.is_delta = use_delta_this_frame + frame_descriptor.is_transparent = False + frame_descriptor.format = format_['image_format_byte'] + frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t + frame_descriptor.delay = frame.info.get('duration', 1000) # If we're not an animation, just pretend we're delaying for 1000ms + frame_descriptor.write(fp) + + # Write out the palette if required + if format_['has_palette']: + palette = graphic_data[0] + palette_descriptor = QGFFramePaletteDescriptorV1() + + # Convert all palette entries to HSV888 and write to the output + palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette)) + vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h') + palette_descriptor.write(fp) + + # Write out the delta info if required + if use_delta_this_frame: + # Set up the rendering location of where the delta frame should be situated + delta_descriptor = QGFFrameDeltaDescriptorV1() + delta_descriptor.bbox = bbox + + # Write the delta frame to the output + vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h') + delta_descriptor.write(fp) + + # Write out the data for this frame to the output + data_descriptor = QGFFrameDataDescriptorV1() + data_descriptor.data = image_data + vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h') + data_descriptor.write(fp) + + +def _save(im, fp, _filename): """Helper method used by PIL to write to an output file. """ # Work out from the parameters if we need to do anything special encoderinfo = im.encoderinfo.copy() - append_images = list(encoderinfo.get("append_images", [])) - verbose = encoderinfo.get("verbose", False) - use_deltas = encoderinfo.get("use_deltas", True) - use_rle = encoderinfo.get("use_rle", True) - # Helper for inline verbose prints - def vprint(s): - if verbose: - print(s) + # Helper for prints, noop taking any args if not verbose + global vprint + verbose = encoderinfo.get("verbose", False) + vprint = print if verbose else lambda *_args, **_kwargs: None # Helper to iterate through all frames in the input image - def _for_all_frames(x: FunctionType): - frame_num = 0 - last_frame = None - for frame in [im] + append_images: - # Get number of of frames in this image - nfr = getattr(frame, "n_frames", 1) - for idx in range(nfr): - frame.seek(idx) - frame.load() - copy = frame.copy().convert("RGB") - x(frame_num, copy, last_frame) - last_frame = copy - frame_num += 1 + append_images = list(encoderinfo.get("append_images", [])) + for_all_frames = functools.partial(_for_all_frames, images=[im, *append_images]) # Collect all the frame sizes frame_sizes = [] - _for_all_frames(lambda idx, frame, last_frame: frame_sizes.append(frame.size)) + for_all_frames(lambda _idx, frame, _last_frame: frame_sizes.append(frame.size)) # Make sure all frames are the same size - if len(list(set(frame_sizes))) != 1: + if len(set(frame_sizes)) != 1: raise ValueError("Mismatching sizes on frames") # Write out the initial graphics descriptor (and write a dummy value), so that we can come back and fill in the @@ -264,8 +405,7 @@ def _save(im, fp, filename): graphics_descriptor_location = fp.tell() graphics_descriptor = QGFGraphicsDescriptor() graphics_descriptor.frame_count = len(frame_sizes) - graphics_descriptor.image_width = frame_sizes[0][0] - graphics_descriptor.image_height = frame_sizes[0][1] + graphics_descriptor.image_size = frame_sizes[0] vprint(f'{"Graphics descriptor block":26s} {fp.tell():5d}d / {fp.tell():04X}h') graphics_descriptor.write(fp) @@ -276,117 +416,9 @@ def _save(im, fp, filename): vprint(f'{"Frame offsets block":26s} {fp.tell():5d}d / {fp.tell():04X}h') frame_offsets.write(fp) - # Helper function to save each frame to the output file - def _write_frame(idx, frame, last_frame): - # If we replace the frame we're going to output with a delta, we can override it here - this_frame = frame - location = (0, 0) - size = frame.size - - # Work out the format we're going to use - format = encoderinfo["qmk_format"] - - # Convert the original frame so we can do comparisons - converted = qmk.painter.convert_requested_format(this_frame, format) - graphic_data = qmk.painter.convert_image_bytes(converted, format) - - # Convert the raw data to RLE-encoded if requested - raw_data = graphic_data[1] - if use_rle: - rle_data = qmk.painter.compress_bytes_qmk_rle(graphic_data[1]) - use_raw_this_frame = not use_rle or len(raw_data) <= len(rle_data) - image_data = raw_data if use_raw_this_frame else rle_data - - # Work out if a delta frame is smaller than injecting it directly - use_delta_this_frame = False - if use_deltas and last_frame is not None: - # If we want to use deltas, then find the difference - diff = ImageChops.difference(frame, last_frame) - - # Get the bounding box of those differences - bbox = diff.getbbox() - - # If we have a valid bounding box... - if bbox: - # ...create the delta frame by cropping the original. - delta_frame = frame.crop(bbox) - delta_location = (bbox[0], bbox[1]) - delta_size = (bbox[2] - bbox[0], bbox[3] - bbox[1]) - - # Convert the delta frame to the requested format - delta_converted = qmk.painter.convert_requested_format(delta_frame, format) - delta_graphic_data = qmk.painter.convert_image_bytes(delta_converted, format) - - # Work out how large the delta frame is going to be with compression etc. - delta_raw_data = delta_graphic_data[1] - if use_rle: - delta_rle_data = qmk.painter.compress_bytes_qmk_rle(delta_graphic_data[1]) - delta_use_raw_this_frame = not use_rle or len(delta_raw_data) <= len(delta_rle_data) - delta_image_data = delta_raw_data if delta_use_raw_this_frame else delta_rle_data - - # If the size of the delta frame (plus delta descriptor) is smaller than the original, use that instead - # This ensures that if a non-delta is overall smaller in size, we use that in preference due to flash - # sizing constraints. - if (len(delta_image_data) + QGFFrameDeltaDescriptorV1.length) < len(image_data): - # Copy across all the delta equivalents so that the rest of the processing acts on those - this_frame = delta_frame - location = delta_location - size = delta_size - converted = delta_converted - graphic_data = delta_graphic_data - raw_data = delta_raw_data - rle_data = delta_rle_data - use_raw_this_frame = delta_use_raw_this_frame - image_data = delta_image_data - use_delta_this_frame = True - - # Write out the frame descriptor - frame_offsets.frame_offsets[idx] = fp.tell() - vprint(f'{f"Frame {idx:3d} base":26s} {fp.tell():5d}d / {fp.tell():04X}h') - frame_descriptor = QGFFrameDescriptorV1() - frame_descriptor.is_delta = use_delta_this_frame - frame_descriptor.is_transparent = False - frame_descriptor.format = format['image_format_byte'] - frame_descriptor.compression = 0x00 if use_raw_this_frame else 0x01 # See qp.h, painter_compression_t - frame_descriptor.delay = frame.info['duration'] if 'duration' in frame.info else 1000 # If we're not an animation, just pretend we're delaying for 1000ms - frame_descriptor.write(fp) - - # Write out the palette if required - if format['has_palette']: - palette = graphic_data[0] - palette_descriptor = QGFFramePaletteDescriptorV1() - - # Helper to convert from RGB888 to the QMK "dialect" of HSV888 - def rgb888_to_qmk_hsv888(e): - hsv = rgb_to_hsv(e[0] / 255.0, e[1] / 255.0, e[2] / 255.0) - return (int(hsv[0] * 255.0), int(hsv[1] * 255.0), int(hsv[2] * 255.0)) - - # Convert all palette entries to HSV888 and write to the output - palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, palette)) - vprint(f'{f"Frame {idx:3d} palette":26s} {fp.tell():5d}d / {fp.tell():04X}h') - palette_descriptor.write(fp) - - # Write out the delta info if required - if use_delta_this_frame: - # Set up the rendering location of where the delta frame should be situated - delta_descriptor = QGFFrameDeltaDescriptorV1() - delta_descriptor.left = location[0] - delta_descriptor.top = location[1] - delta_descriptor.right = location[0] + size[0] - 1 - delta_descriptor.bottom = location[1] + size[1] - 1 - - # Write the delta frame to the output - vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h') - delta_descriptor.write(fp) - - # Write out the data for this frame to the output - data_descriptor = QGFFrameDataDescriptorV1() - data_descriptor.data = image_data - vprint(f'{f"Frame {idx:3d} data":26s} {fp.tell():5d}d / {fp.tell():04X}h') - data_descriptor.write(fp) - # Iterate over each if the input frames, writing it to the output in the process - _for_all_frames(_write_frame) + write_frame = functools.partial(_write_frame, format_=encoderinfo["qmk_format"], fp=fp, use_deltas=encoderinfo.get("use_deltas", True), use_rle=encoderinfo.get("use_rle", True), frame_offsets=frame_offsets) + for_all_frames(write_frame) # Go back and update the graphics descriptor now that we can determine the final file size graphics_descriptor.total_file_size = fp.tell() diff --git a/lib/python/qmk/search.py b/lib/python/qmk/search.py index 84cf6cbe32..33550a3db2 100644 --- a/lib/python/qmk/search.py +++ b/lib/python/qmk/search.py @@ -5,7 +5,7 @@ import functools import fnmatch import logging import re -from typing import List, Tuple +from typing import Callable, List, Optional, Tuple from dotty_dict import dotty, Dotty from milc import cli @@ -15,6 +15,82 @@ from qmk.keyboard import list_keyboards, keyboard_folder from qmk.keymap import list_keymaps, locate_keymap from qmk.build_targets import KeyboardKeymapBuildTarget, BuildTarget +TargetInfo = Tuple[str, str, dict] + + +# by using a class for filters, we dont need to worry about capturing values +# see details <https://github.com/qmk/qmk_firmware/pull/21090> +class FilterFunction: + """Base class for filters. + It provides: + - __init__: capture key and value + + Each subclass should provide: + - func_name: how it will be specified on CLI + >>> qmk find -f <func_name>... + - apply: function that actually applies the filter + ie: return whether the input kb/km satisfies the condition + """ + + key: str + value: Optional[str] + + func_name: str + apply: Callable[[TargetInfo], bool] + + def __init__(self, key, value): + self.key = key + self.value = value + + +class Exists(FilterFunction): + func_name = "exists" + + def apply(self, target_info: TargetInfo) -> bool: + _kb, _km, info = target_info + return self.key in info + + +class Absent(FilterFunction): + func_name = "absent" + + def apply(self, target_info: TargetInfo) -> bool: + _kb, _km, info = target_info + return self.key not in info + + +class Length(FilterFunction): + func_name = "length" + + def apply(self, target_info: TargetInfo) -> bool: + _kb, _km, info = target_info + return (self.key in info and len(info[self.key]) == int(self.value)) + + +class Contains(FilterFunction): + func_name = "contains" + + def apply(self, target_info: TargetInfo) -> bool: + _kb, _km, info = target_info + return (self.key in info and self.value in info[self.key]) + + +def _get_filter_class(func_name: str, key: str, value: str) -> Optional[FilterFunction]: + """Initialize a filter subclass based on regex findings and return it. + None if no there's no filter with the name queried. + """ + + for subclass in FilterFunction.__subclasses__(): + if func_name == subclass.func_name: + return subclass(key, value) + + return None + + +def filter_help() -> str: + names = [f"'{f.func_name}'" for f in FilterFunction.__subclasses__()] + return ", ".join(names[:-1]) + f" and {names[-1]}" + def _set_log_level(level): cli.acquire_lock() @@ -48,11 +124,12 @@ def _keymap_exists(keyboard, keymap): return keyboard if locate_keymap(keyboard, keymap) is not None else None -def _load_keymap_info(kb_km): +def _load_keymap_info(target: Tuple[str, str]) -> TargetInfo: """Returns a tuple of (keyboard, keymap, info.json) for the given keyboard/keymap combination. """ + kb, km = target with ignore_logging(): - return (kb_km[0], kb_km[1], keymap_json(kb_km[0], kb_km[1])) + return (kb, km, keymap_json(kb, km)) def expand_make_targets(targets: List[str]) -> List[Tuple[str, str]]: @@ -139,26 +216,14 @@ def _filter_keymap_targets(target_list: List[Tuple[str, str]], filters: List[str key = function_match.group('key') value = function_match.group('value') - if value is not None: - if func_name == 'length': - valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and len(e[2].get(key)) == int(value), valid_keymaps) - elif func_name == 'contains': - valid_keymaps = filter(lambda e, key=key, value=value: key in e[2] and value in e[2].get(key), valid_keymaps) - else: - cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') - continue - - cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}, {{fg_cyan}}{value}{{fg_reset}})...') - else: - if func_name == 'exists': - valid_keymaps = filter(lambda e, key=key: key in e[2], valid_keymaps) - elif func_name == 'absent': - valid_keymaps = filter(lambda e, key=key: key not in e[2], valid_keymaps) - else: - cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') - continue - - cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}})...') + filter_class = _get_filter_class(func_name, key, value) + if filter_class is None: + cli.log.warning(f'Unrecognized filter expression: {function_match.group(0)}') + continue + valid_keymaps = filter(filter_class.apply, valid_keymaps) + + value_str = f", {{fg_cyan}}{value}{{fg_reset}})" if value is not None else "" + cli.log.info(f'Filtering on condition: {{fg_green}}{func_name}{{fg_reset}}({{fg_cyan}}{key}{{fg_reset}}{value_str}...') elif equals_match is not None: key = equals_match.group('key') |