summaryrefslogtreecommitdiff
path: root/lib/python
diff options
context:
space:
mode:
authorPablo Martínez <58857054+elpekenin@users.noreply.github.com>2024-03-10 01:29:09 +0100
committerGitHub <noreply@github.com>2024-03-10 00:29:09 +0000
commitc5225ab5009476c60a9cb27837615d4f29c9b19a (patch)
tree0dc68687abe26995d3a655e83c34f703885ffebf /lib/python
parent729520f302e905e2b3e6711b1fc6d0061843e355 (diff)
[Feature] Some metadata on QGF/QFF files (#20101)
Diffstat (limited to 'lib/python')
-rw-r--r--lib/python/qmk/cli/painter/convert_graphics.py34
-rw-r--r--lib/python/qmk/cli/painter/make_font.py36
-rw-r--r--lib/python/qmk/painter.py102
-rw-r--r--lib/python/qmk/painter_qgf.py26
4 files changed, 146 insertions, 52 deletions
diff --git a/lib/python/qmk/cli/painter/convert_graphics.py b/lib/python/qmk/cli/painter/convert_graphics.py
index 2519c49b25..553c26aa5d 100644
--- a/lib/python/qmk/cli/painter/convert_graphics.py
+++ b/lib/python/qmk/cli/painter/convert_graphics.py
@@ -1,10 +1,8 @@
1"""This script tests QGF functionality. 1"""This script tests QGF functionality.
2""" 2"""
3import re
4import datetime
5from io import BytesIO 3from io import BytesIO
6from qmk.path import normpath 4from qmk.path import normpath
7from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats 5from qmk.painter import generate_subs, render_header, render_source, valid_formats
8from milc import cli 6from milc import cli
9from PIL import Image 7from PIL import Image
10 8
@@ -12,7 +10,7 @@ from PIL import Image
12@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.') 10@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.')
13@cli.argument('-i', '--input', required=True, help='Specify input graphic file.') 11@cli.argument('-i', '--input', required=True, help='Specify input graphic file.')
14@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.') 12@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
15@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys()))) 13@cli.argument('-f', '--format', required=True, help=f'Output format, valid types: {", ".join(valid_formats.keys())}')
16@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.') 14@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.')
17@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.') 15@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.')
18@cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QGF file as raw data instead of c/h combo.') 16@cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QGF file as raw data instead of c/h combo.')
@@ -51,43 +49,31 @@ def painter_convert_graphics(cli):
51 49
52 # Convert the image to QGF using PIL 50 # Convert the image to QGF using PIL
53 out_data = BytesIO() 51 out_data = BytesIO()
54 input_img.save(out_data, "QGF", use_deltas=(not cli.args.no_deltas), use_rle=(not cli.args.no_rle), qmk_format=format, verbose=cli.args.verbose) 52 metadata = []
53 input_img.save(out_data, "QGF", use_deltas=(not cli.args.no_deltas), use_rle=(not cli.args.no_rle), qmk_format=format, verbose=cli.args.verbose, metadata=metadata)
55 out_bytes = out_data.getvalue() 54 out_bytes = out_data.getvalue()
56 55
57 if cli.args.raw: 56 if cli.args.raw:
58 raw_file = cli.args.output / (cli.args.input.stem + ".qgf") 57 raw_file = cli.args.output / f"{cli.args.input.stem}.qgf"
59 with open(raw_file, 'wb') as raw: 58 with open(raw_file, 'wb') as raw:
60 raw.write(out_bytes) 59 raw.write(out_bytes)
61 return 60 return
62 61
63 # Work out the text substitutions for rendering the output data 62 # Work out the text substitutions for rendering the output data
64 subs = { 63 args_str = " ".join((f"--{arg} {getattr(cli.args, arg.replace('-', '_'))}" for arg in ["input", "output", "format", "no-rle", "no-deltas"]))
65 'generated_type': 'image', 64 command = f"qmk painter-convert-graphics {args_str}"
66 'var_prefix': 'gfx', 65 subs = generate_subs(cli, out_bytes, image_metadata=metadata, command=command)
67 'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}',
68 'year': datetime.date.today().strftime("%Y"),
69 'input_file': cli.args.input.name,
70 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
71 'byte_count': len(out_bytes),
72 'bytes_lines': render_bytes(out_bytes),
73 'format': cli.args.format,
74 }
75
76 # Render the license
77 subs.update({'license': render_license(subs)})
78 66
79 # Render and write the header file 67 # Render and write the header file
80 header_text = render_header(subs) 68 header_text = render_header(subs)
81 header_file = cli.args.output / (cli.args.input.stem + ".qgf.h") 69 header_file = cli.args.output / f"{cli.args.input.stem}.qgf.h"
82 with open(header_file, 'w') as header: 70 with open(header_file, 'w') as header:
83 print(f"Writing {header_file}...") 71 print(f"Writing {header_file}...")
84 header.write(header_text) 72 header.write(header_text)
85 header.close()
86 73
87 # Render and write the source file 74 # Render and write the source file
88 source_text = render_source(subs) 75 source_text = render_source(subs)
89 source_file = cli.args.output / (cli.args.input.stem + ".qgf.c") 76 source_file = cli.args.output / f"{cli.args.input.stem}.qgf.c"
90 with open(source_file, 'w') as source: 77 with open(source_file, 'w') as source:
91 print(f"Writing {source_file}...") 78 print(f"Writing {source_file}...")
92 source.write(source_text) 79 source.write(source_text)
93 source.close()
diff --git a/lib/python/qmk/cli/painter/make_font.py b/lib/python/qmk/cli/painter/make_font.py
index c0189920d2..19db844931 100644
--- a/lib/python/qmk/cli/painter/make_font.py
+++ b/lib/python/qmk/cli/painter/make_font.py
@@ -1,12 +1,10 @@
1"""This script automates the conversion of font files into a format QMK firmware understands. 1"""This script automates the conversion of font files into a format QMK firmware understands.
2""" 2"""
3 3
4import re
5import datetime
6from io import BytesIO 4from io import BytesIO
7from qmk.path import normpath 5from qmk.path import normpath
8from qmk.painter_qff import QFFFont 6from qmk.painter_qff import _generate_font_glyphs_list, QFFFont
9from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats 7from qmk.painter import generate_subs, render_header, render_source, valid_formats
10from milc import cli 8from milc import cli
11 9
12 10
@@ -31,7 +29,7 @@ def painter_make_font_image(cli):
31@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.') 29@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
32@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.') 30@cli.argument('-n', '--no-ascii', arg_only=True, action='store_true', help='Disables output of the full ASCII character set (0x20..0x7E), exporting only the glyphs specified.')
33@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.') 31@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
34@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys()))) 32@cli.argument('-f', '--format', required=True, help=f'Output format, valid types: {", ".join(valid_formats.keys())}')
35@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.') 33@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.')
36@cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QFF file as raw data instead of c/h combo.') 34@cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QFF file as raw data instead of c/h combo.')
37@cli.subcommand('Converts an input font image to something QMK firmware understands') 35@cli.subcommand('Converts an input font image to something QMK firmware understands')
@@ -53,43 +51,31 @@ def painter_convert_font_image(cli):
53 51
54 # Render out the data 52 # Render out the data
55 out_data = BytesIO() 53 out_data = BytesIO()
56 font.save_to_qff(format, (False if cli.args.no_rle else True), out_data) 54 font.save_to_qff(format, not cli.args.no_rle, out_data)
57 out_bytes = out_data.getvalue() 55 out_bytes = out_data.getvalue()
58 56
59 if cli.args.raw: 57 if cli.args.raw:
60 raw_file = cli.args.output / (cli.args.input.stem + ".qff") 58 raw_file = cli.args.output / f"{cli.args.input.stem}.qff"
61 with open(raw_file, 'wb') as raw: 59 with open(raw_file, 'wb') as raw:
62 raw.write(out_bytes) 60 raw.write(out_bytes)
63 return 61 return
64 62
65 # Work out the text substitutions for rendering the output data 63 # Work out the text substitutions for rendering the output data
66 subs = { 64 args_str = " ".join((f"--{arg} {getattr(cli.args, arg.replace('-', '_'))}" for arg in ["input", "output", "no-ascii", "unicode-glyphs", "format", "no-rle"]))
67 'generated_type': 'font', 65 command = f"qmk painter-convert-font-image {args_str}"
68 'var_prefix': 'font', 66 metadata = {"glyphs": _generate_font_glyphs_list(not cli.args.no_ascii, cli.args.unicode_glyphs)}
69 'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}', 67 subs = generate_subs(cli, out_bytes, font_metadata=metadata, command=command)
70 'year': datetime.date.today().strftime("%Y"),
71 'input_file': cli.args.input.name,
72 'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
73 'byte_count': len(out_bytes),
74 'bytes_lines': render_bytes(out_bytes),
75 'format': cli.args.format,
76 }
77
78 # Render the license
79 subs.update({'license': render_license(subs)})
80 68
81 # Render and write the header file 69 # Render and write the header file
82 header_text = render_header(subs) 70 header_text = render_header(subs)
83 header_file = cli.args.output / (cli.args.input.stem + ".qff.h") 71 header_file = cli.args.output / f"{cli.args.input.stem}.qff.h"
84 with open(header_file, 'w') as header: 72 with open(header_file, 'w') as header:
85 print(f"Writing {header_file}...") 73 print(f"Writing {header_file}...")
86 header.write(header_text) 74 header.write(header_text)
87 header.close()
88 75
89 # Render and write the source file 76 # Render and write the source file
90 source_text = render_source(subs) 77 source_text = render_source(subs)
91 source_file = cli.args.output / (cli.args.input.stem + ".qff.c") 78 source_file = cli.args.output / f"{cli.args.input.stem}.qff.c"
92 with open(source_file, 'w') as source: 79 with open(source_file, 'w') as source:
93 print(f"Writing {source_file}...") 80 print(f"Writing {source_file}...")
94 source.write(source_text) 81 source.write(source_text)
95 source.close()
diff --git a/lib/python/qmk/painter.py b/lib/python/qmk/painter.py
index 381a996443..512a486ce8 100644
--- a/lib/python/qmk/painter.py
+++ b/lib/python/qmk/painter.py
@@ -1,5 +1,6 @@
1"""Functions that help us work with Quantum Painter's file formats. 1"""Functions that help us work with Quantum Painter's file formats.
2""" 2"""
3import datetime
3import math 4import math
4import re 5import re
5from string import Template 6from string import Template
@@ -79,6 +80,105 @@ valid_formats = {
79 } 80 }
80} 81}
81 82
83
84def _render_text(values):
85 # FIXME: May need more chars with GIFs containing lots of frames (or longer durations)
86 return "|".join([f"{i:4d}" for i in values])
87
88
89def _render_numeration(metadata):
90 return _render_text(range(len(metadata)))
91
92
93def _render_values(metadata, key):
94 return _render_text([i[key] for i in metadata])
95
96
97def _render_image_metadata(metadata):
98 size = metadata.pop(0)
99
100 lines = [
101 "// Image's metadata",
102 "// ----------------",
103 f"// Width: {size['width']}",
104 f"// Height: {size['height']}",
105 ]
106
107 if len(metadata) == 1:
108 lines.append("// Single frame")
109
110 else:
111 lines.extend([
112 f"// Frame: {_render_numeration(metadata)}",
113 f"// Duration(ms): {_render_values(metadata, 'delay')}",
114 f"// Compression: {_render_values(metadata, 'compression')} >> See qp.h, painter_compression_t",
115 f"// Delta: {_render_values(metadata, 'delta')}",
116 ])
117
118 deltas = []
119 for i, v in enumerate(metadata):
120 # Not a delta frame, go to next one
121 if not v["delta"]:
122 continue
123
124 # Unpack rect's coords
125 l, t, r, b = v["delta_rect"]
126
127 delta_px = (r - l) * (b - t)
128 px = size["width"] * size["height"]
129
130 # FIXME: May need need more chars here too
131 deltas.append(f"// Frame {i:3d}: ({l:3d}, {t:3d}) - ({r:3d}, {b:3d}) >> {delta_px:4d}/{px:4d} pixels ({100*delta_px/px:.2f}%)")
132
133 if deltas:
134 lines.append("// Areas on delta frames")
135 lines.extend(deltas)
136
137 return "\n".join(lines)
138
139
140def generate_subs(cli, out_bytes, *, font_metadata=None, image_metadata=None, command):
141 if font_metadata is not None and image_metadata is not None:
142 raise ValueError("Cant generate subs for font and image at the same time")
143
144 subs = {
145 "year": datetime.date.today().strftime("%Y"),
146 "input_file": cli.args.input.name,
147 "sane_name": re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
148 "byte_count": len(out_bytes),
149 "bytes_lines": render_bytes(out_bytes),
150 "format": cli.args.format,
151 "generator_command": command,
152 }
153
154 if font_metadata is not None:
155 subs.update({
156 "generated_type": "font",
157 "var_prefix": "font",
158 # not using triple quotes to avoid extra indentation/weird formatted code
159 "metadata": "\n".join([
160 "// Font's metadata",
161 "// ---------------",
162 f"// Glyphs: {', '.join([i for i in font_metadata['glyphs']])}",
163 ]),
164 })
165
166 elif image_metadata is not None:
167 subs.update({
168 "generated_type": "image",
169 "var_prefix": "gfx",
170 "generator_command": command,
171 "metadata": _render_image_metadata(image_metadata),
172 })
173
174 else:
175 raise ValueError("Pass metadata for either an image or a font")
176
177 subs.update({"license": render_license(subs)})
178
179 return subs
180
181
82license_template = """\ 182license_template = """\
83// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright 183// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
84// SPDX-License-Identifier: GPL-2.0-or-later 184// SPDX-License-Identifier: GPL-2.0-or-later
@@ -110,6 +210,8 @@ def render_header(subs):
110 210
111source_file_template = """\ 211source_file_template = """\
112${license} 212${license}
213${metadata}
214
113#include <qp.h> 215#include <qp.h>
114 216
115const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count}; 217const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};
diff --git a/lib/python/qmk/painter_qgf.py b/lib/python/qmk/painter_qgf.py
index cc4697f1c6..67ef0dd233 100644
--- a/lib/python/qmk/painter_qgf.py
+++ b/lib/python/qmk/painter_qgf.py
@@ -327,8 +327,9 @@ def _compress_image(frame, last_frame, *, use_rle, use_deltas, format_, **_kwarg
327 327
328 328
329# Helper function to save each frame to the output file 329# Helper function to save each frame to the output file
330def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, **kwargs): 330def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, metadata, **kwargs):
331 # Not an argument of the function as it would consume from **kwargs 331 # Not an argument of the function as it would then not be part of kwargs
332 # This would cause an issue with `_compress_image(**kwargs)` missing an argument
332 format_ = kwargs["format_"] 333 format_ = kwargs["format_"]
333 334
334 # (potentially) Apply RLE and/or delta, and work out output image's information 335 # (potentially) Apply RLE and/or delta, and work out output image's information
@@ -370,6 +371,21 @@ def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, **kwargs):
370 vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h') 371 vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
371 delta_descriptor.write(fp) 372 delta_descriptor.write(fp)
372 373
374 # Store metadata, showed later in a comment in the generated file
375 frame_metadata = {
376 "compression": frame_descriptor.compression,
377 "delta": frame_descriptor.is_delta,
378 "delay": frame_descriptor.delay,
379 }
380 if frame_metadata["delta"]:
381 frame_metadata.update({"delta_rect": [
382 delta_descriptor.left,
383 delta_descriptor.top,
384 delta_descriptor.right,
385 delta_descriptor.bottom,
386 ]})
387 metadata.append(frame_metadata)
388
373 # Write out the data for this frame to the output 389 # Write out the data for this frame to the output
374 data_descriptor = QGFFrameDataDescriptorV1() 390 data_descriptor = QGFFrameDataDescriptorV1()
375 data_descriptor.data = image_data 391 data_descriptor.data = image_data
@@ -383,6 +399,10 @@ def _save(im, fp, _filename):
383 # Work out from the parameters if we need to do anything special 399 # Work out from the parameters if we need to do anything special
384 encoderinfo = im.encoderinfo.copy() 400 encoderinfo = im.encoderinfo.copy()
385 401
402 # Store image file in metadata structure
403 metadata = encoderinfo.get("metadata", [])
404 metadata.append({"width": im.width, "height": im.height})
405
386 # Helper for prints, noop taking any args if not verbose 406 # Helper for prints, noop taking any args if not verbose
387 global vprint 407 global vprint
388 verbose = encoderinfo.get("verbose", False) 408 verbose = encoderinfo.get("verbose", False)
@@ -417,7 +437,7 @@ def _save(im, fp, _filename):
417 frame_offsets.write(fp) 437 frame_offsets.write(fp)
418 438
419 # Iterate over each if the input frames, writing it to the output in the process 439 # Iterate over each if the input frames, writing it to the output in the process
420 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) 440 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, metadata=metadata)
421 for_all_frames(write_frame) 441 for_all_frames(write_frame)
422 442
423 # Go back and update the graphics descriptor now that we can determine the final file size 443 # Go back and update the graphics descriptor now that we can determine the final file size