[Feature] Some metadata on QGF/QFF files (#20101)

This commit is contained in:
Pablo Martínez 2024-03-10 01:29:09 +01:00 committed by GitHub
parent 729520f302
commit c5225ab500
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 146 additions and 52 deletions

View file

@ -1,10 +1,8 @@
"""This script tests QGF functionality. """This script tests QGF functionality.
""" """
import re
import datetime
from io import BytesIO from io import BytesIO
from qmk.path import normpath from qmk.path import normpath
from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats from qmk.painter import generate_subs, render_header, render_source, valid_formats
from milc import cli from milc import cli
from PIL import Image from PIL import Image
@ -12,7 +10,7 @@ from PIL import Image
@cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.') @cli.argument('-v', '--verbose', arg_only=True, action='store_true', help='Turns on verbose output.')
@cli.argument('-i', '--input', required=True, help='Specify input graphic file.') @cli.argument('-i', '--input', required=True, help='Specify input graphic file.')
@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.') @cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys()))) @cli.argument('-f', '--format', required=True, help=f'Output format, valid types: {", ".join(valid_formats.keys())}')
@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.') @cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disables the use of RLE when encoding images.')
@cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.') @cli.argument('-d', '--no-deltas', arg_only=True, action='store_true', help='Disables the use of delta frames when encoding animations.')
@cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QGF file as raw data instead of c/h combo.') @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):
# Convert the image to QGF using PIL # Convert the image to QGF using PIL
out_data = BytesIO() out_data = BytesIO()
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 = []
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)
out_bytes = out_data.getvalue() out_bytes = out_data.getvalue()
if cli.args.raw: if cli.args.raw:
raw_file = cli.args.output / (cli.args.input.stem + ".qgf") raw_file = cli.args.output / f"{cli.args.input.stem}.qgf"
with open(raw_file, 'wb') as raw: with open(raw_file, 'wb') as raw:
raw.write(out_bytes) raw.write(out_bytes)
return return
# Work out the text substitutions for rendering the output data # Work out the text substitutions for rendering the output data
subs = { args_str = " ".join((f"--{arg} {getattr(cli.args, arg.replace('-', '_'))}" for arg in ["input", "output", "format", "no-rle", "no-deltas"]))
'generated_type': 'image', command = f"qmk painter-convert-graphics {args_str}"
'var_prefix': 'gfx', subs = generate_subs(cli, out_bytes, image_metadata=metadata, command=command)
'generator_command': f'qmk painter-convert-graphics -i {cli.args.input.name} -f {cli.args.format}',
'year': datetime.date.today().strftime("%Y"),
'input_file': cli.args.input.name,
'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
'byte_count': len(out_bytes),
'bytes_lines': render_bytes(out_bytes),
'format': cli.args.format,
}
# Render the license
subs.update({'license': render_license(subs)})
# Render and write the header file # Render and write the header file
header_text = render_header(subs) header_text = render_header(subs)
header_file = cli.args.output / (cli.args.input.stem + ".qgf.h") header_file = cli.args.output / f"{cli.args.input.stem}.qgf.h"
with open(header_file, 'w') as header: with open(header_file, 'w') as header:
print(f"Writing {header_file}...") print(f"Writing {header_file}...")
header.write(header_text) header.write(header_text)
header.close()
# Render and write the source file # Render and write the source file
source_text = render_source(subs) source_text = render_source(subs)
source_file = cli.args.output / (cli.args.input.stem + ".qgf.c") source_file = cli.args.output / f"{cli.args.input.stem}.qgf.c"
with open(source_file, 'w') as source: with open(source_file, 'w') as source:
print(f"Writing {source_file}...") print(f"Writing {source_file}...")
source.write(source_text) source.write(source_text)
source.close()

View file

@ -1,12 +1,10 @@
"""This script automates the conversion of font files into a format QMK firmware understands. """This script automates the conversion of font files into a format QMK firmware understands.
""" """
import re
import datetime
from io import BytesIO from io import BytesIO
from qmk.path import normpath from qmk.path import normpath
from qmk.painter_qff import QFFFont from qmk.painter_qff import _generate_font_glyphs_list, QFFFont
from qmk.painter import render_header, render_source, render_license, render_bytes, valid_formats from qmk.painter import generate_subs, render_header, render_source, valid_formats
from milc import cli from milc import cli
@ -31,7 +29,7 @@ def painter_make_font_image(cli):
@cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.') @cli.argument('-o', '--output', default='', help='Specify output directory. Defaults to same directory as input.')
@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.') @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.')
@cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.') @cli.argument('-u', '--unicode-glyphs', default='', help='Also generate the specified unicode glyphs.')
@cli.argument('-f', '--format', required=True, help='Output format, valid types: %s' % (', '.join(valid_formats.keys()))) @cli.argument('-f', '--format', required=True, help=f'Output format, valid types: {", ".join(valid_formats.keys())}')
@cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.') @cli.argument('-r', '--no-rle', arg_only=True, action='store_true', help='Disable the use of RLE to minimise converted image size.')
@cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QFF file as raw data instead of c/h combo.') @cli.argument('-w', '--raw', arg_only=True, action='store_true', help='Writes out the QFF file as raw data instead of c/h combo.')
@cli.subcommand('Converts an input font image to something QMK firmware understands') @cli.subcommand('Converts an input font image to something QMK firmware understands')
@ -53,43 +51,31 @@ def painter_convert_font_image(cli):
# Render out the data # Render out the data
out_data = BytesIO() out_data = BytesIO()
font.save_to_qff(format, (False if cli.args.no_rle else True), out_data) font.save_to_qff(format, not cli.args.no_rle, out_data)
out_bytes = out_data.getvalue() out_bytes = out_data.getvalue()
if cli.args.raw: if cli.args.raw:
raw_file = cli.args.output / (cli.args.input.stem + ".qff") raw_file = cli.args.output / f"{cli.args.input.stem}.qff"
with open(raw_file, 'wb') as raw: with open(raw_file, 'wb') as raw:
raw.write(out_bytes) raw.write(out_bytes)
return return
# Work out the text substitutions for rendering the output data # Work out the text substitutions for rendering the output data
subs = { args_str = " ".join((f"--{arg} {getattr(cli.args, arg.replace('-', '_'))}" for arg in ["input", "output", "no-ascii", "unicode-glyphs", "format", "no-rle"]))
'generated_type': 'font', command = f"qmk painter-convert-font-image {args_str}"
'var_prefix': 'font', metadata = {"glyphs": _generate_font_glyphs_list(not cli.args.no_ascii, cli.args.unicode_glyphs)}
'generator_command': f'qmk painter-convert-font-image -i {cli.args.input.name} -f {cli.args.format}', subs = generate_subs(cli, out_bytes, font_metadata=metadata, command=command)
'year': datetime.date.today().strftime("%Y"),
'input_file': cli.args.input.name,
'sane_name': re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
'byte_count': len(out_bytes),
'bytes_lines': render_bytes(out_bytes),
'format': cli.args.format,
}
# Render the license
subs.update({'license': render_license(subs)})
# Render and write the header file # Render and write the header file
header_text = render_header(subs) header_text = render_header(subs)
header_file = cli.args.output / (cli.args.input.stem + ".qff.h") header_file = cli.args.output / f"{cli.args.input.stem}.qff.h"
with open(header_file, 'w') as header: with open(header_file, 'w') as header:
print(f"Writing {header_file}...") print(f"Writing {header_file}...")
header.write(header_text) header.write(header_text)
header.close()
# Render and write the source file # Render and write the source file
source_text = render_source(subs) source_text = render_source(subs)
source_file = cli.args.output / (cli.args.input.stem + ".qff.c") source_file = cli.args.output / f"{cli.args.input.stem}.qff.c"
with open(source_file, 'w') as source: with open(source_file, 'w') as source:
print(f"Writing {source_file}...") print(f"Writing {source_file}...")
source.write(source_text) source.write(source_text)
source.close()

View file

@ -1,5 +1,6 @@
"""Functions that help us work with Quantum Painter's file formats. """Functions that help us work with Quantum Painter's file formats.
""" """
import datetime
import math import math
import re import re
from string import Template from string import Template
@ -79,6 +80,105 @@ valid_formats = {
} }
} }
def _render_text(values):
# FIXME: May need more chars with GIFs containing lots of frames (or longer durations)
return "|".join([f"{i:4d}" for i in values])
def _render_numeration(metadata):
return _render_text(range(len(metadata)))
def _render_values(metadata, key):
return _render_text([i[key] for i in metadata])
def _render_image_metadata(metadata):
size = metadata.pop(0)
lines = [
"// Image's metadata",
"// ----------------",
f"// Width: {size['width']}",
f"// Height: {size['height']}",
]
if len(metadata) == 1:
lines.append("// Single frame")
else:
lines.extend([
f"// Frame: {_render_numeration(metadata)}",
f"// Duration(ms): {_render_values(metadata, 'delay')}",
f"// Compression: {_render_values(metadata, 'compression')} >> See qp.h, painter_compression_t",
f"// Delta: {_render_values(metadata, 'delta')}",
])
deltas = []
for i, v in enumerate(metadata):
# Not a delta frame, go to next one
if not v["delta"]:
continue
# Unpack rect's coords
l, t, r, b = v["delta_rect"]
delta_px = (r - l) * (b - t)
px = size["width"] * size["height"]
# FIXME: May need need more chars here too
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}%)")
if deltas:
lines.append("// Areas on delta frames")
lines.extend(deltas)
return "\n".join(lines)
def generate_subs(cli, out_bytes, *, font_metadata=None, image_metadata=None, command):
if font_metadata is not None and image_metadata is not None:
raise ValueError("Cant generate subs for font and image at the same time")
subs = {
"year": datetime.date.today().strftime("%Y"),
"input_file": cli.args.input.name,
"sane_name": re.sub(r"[^a-zA-Z0-9]", "_", cli.args.input.stem),
"byte_count": len(out_bytes),
"bytes_lines": render_bytes(out_bytes),
"format": cli.args.format,
"generator_command": command,
}
if font_metadata is not None:
subs.update({
"generated_type": "font",
"var_prefix": "font",
# not using triple quotes to avoid extra indentation/weird formatted code
"metadata": "\n".join([
"// Font's metadata",
"// ---------------",
f"// Glyphs: {', '.join([i for i in font_metadata['glyphs']])}",
]),
})
elif image_metadata is not None:
subs.update({
"generated_type": "image",
"var_prefix": "gfx",
"generator_command": command,
"metadata": _render_image_metadata(image_metadata),
})
else:
raise ValueError("Pass metadata for either an image or a font")
subs.update({"license": render_license(subs)})
return subs
license_template = """\ license_template = """\
// Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright // Copyright ${year} QMK -- generated source code only, ${generated_type} retains original copyright
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
@ -110,6 +210,8 @@ def render_header(subs):
source_file_template = """\ source_file_template = """\
${license} ${license}
${metadata}
#include <qp.h> #include <qp.h>
const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count}; const uint32_t ${var_prefix}_${sane_name}_length = ${byte_count};

View file

@ -327,8 +327,9 @@ def _compress_image(frame, last_frame, *, use_rle, use_deltas, format_, **_kwarg
# Helper function to save each frame to the output file # Helper function to save each frame to the output file
def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, **kwargs): def _write_frame(idx, frame, last_frame, *, fp, frame_offsets, metadata, **kwargs):
# Not an argument of the function as it would consume from **kwargs # Not an argument of the function as it would then not be part of kwargs
# This would cause an issue with `_compress_image(**kwargs)` missing an argument
format_ = kwargs["format_"] format_ = kwargs["format_"]
# (potentially) Apply RLE and/or delta, and work out output image's information # (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):
vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h') vprint(f'{f"Frame {idx:3d} delta":26s} {fp.tell():5d}d / {fp.tell():04X}h')
delta_descriptor.write(fp) delta_descriptor.write(fp)
# Store metadata, showed later in a comment in the generated file
frame_metadata = {
"compression": frame_descriptor.compression,
"delta": frame_descriptor.is_delta,
"delay": frame_descriptor.delay,
}
if frame_metadata["delta"]:
frame_metadata.update({"delta_rect": [
delta_descriptor.left,
delta_descriptor.top,
delta_descriptor.right,
delta_descriptor.bottom,
]})
metadata.append(frame_metadata)
# Write out the data for this frame to the output # Write out the data for this frame to the output
data_descriptor = QGFFrameDataDescriptorV1() data_descriptor = QGFFrameDataDescriptorV1()
data_descriptor.data = image_data data_descriptor.data = image_data
@ -383,6 +399,10 @@ def _save(im, fp, _filename):
# Work out from the parameters if we need to do anything special # Work out from the parameters if we need to do anything special
encoderinfo = im.encoderinfo.copy() encoderinfo = im.encoderinfo.copy()
# Store image file in metadata structure
metadata = encoderinfo.get("metadata", [])
metadata.append({"width": im.width, "height": im.height})
# Helper for prints, noop taking any args if not verbose # Helper for prints, noop taking any args if not verbose
global vprint global vprint
verbose = encoderinfo.get("verbose", False) verbose = encoderinfo.get("verbose", False)
@ -417,7 +437,7 @@ def _save(im, fp, _filename):
frame_offsets.write(fp) frame_offsets.write(fp)
# Iterate over each if the input frames, writing it to the output in the process # Iterate over each if the input frames, writing it to the output in the process
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) 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)
for_all_frames(write_frame) for_all_frames(write_frame)
# Go back and update the graphics descriptor now that we can determine the final file size # Go back and update the graphics descriptor now that we can determine the final file size