123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401 |
- from pathlib import Path
- from typing import Dict, Any
- from colorsys import rgb_to_hsv
- from PIL import Image, ImageDraw, ImageFont, ImageChops
- from PIL._binary import o8, o16le as o16, o32le as o32
- from qmk.painter_qgf import QGFBlockHeader, QGFFramePaletteDescriptorV1
- from milc.attrdict import AttrDict
- import qmk.painter
- def o24(i):
- return o16(i & 0xFFFF) + o8((i & 0xFF0000) >> 16)
- class QFFGlyphInfo(AttrDict):
- def __init__(self, *args, **kwargs):
- super().__init__()
- for n, value in enumerate(args):
- self[f'arg:{n}'] = value
- for key, value in kwargs.items():
- self[key] = value
- def write(self, fp, include_code_point):
- if include_code_point is True:
- fp.write(o24(ord(self.code_point)))
- value = ((self.data_offset << 6) & 0xFFFFC0) | (self.w & 0x3F)
- fp.write(o24(value))
- class QFFFontDescriptor:
- type_id = 0x00
- length = 20
- magic = 0x464651
- def __init__(self):
- self.header = QGFBlockHeader()
- self.header.type_id = QFFFontDescriptor.type_id
- self.header.length = QFFFontDescriptor.length
- self.version = 1
- self.total_file_size = 0
- self.line_height = 0
- self.has_ascii_table = False
- self.unicode_glyph_count = 0
- self.format = 0xFF
- self.flags = 0
- self.compression = 0xFF
- self.transparency_index = 0xFF
- def write(self, fp):
- self.header.write(fp)
- fp.write(
- b''
- + o24(QFFFontDescriptor.magic)
- + o8(self.version)
- + o32(self.total_file_size)
- + o32((~self.total_file_size) & 0xFFFFFFFF)
- + o8(self.line_height)
- + o8(1 if self.has_ascii_table is True else 0)
- + o16(self.unicode_glyph_count & 0xFFFF)
- + o8(self.format)
- + o8(self.flags)
- + o8(self.compression)
- + o8(self.transparency_index)
- )
- @property
- def is_transparent(self):
- return (self.flags & 0x01) == 0x01
- @is_transparent.setter
- def is_transparent(self, val):
- if val:
- self.flags |= 0x01
- else:
- self.flags &= ~0x01
- class QFFAsciiGlyphTableV1:
- type_id = 0x01
- length = 95 * 3
- def __init__(self):
- self.header = QGFBlockHeader()
- self.header.type_id = QFFAsciiGlyphTableV1.type_id
- self.header.length = QFFAsciiGlyphTableV1.length
-
- self.glyphs = {}
- def add_glyph(self, glyph: QFFGlyphInfo):
- self.glyphs[ord(glyph.code_point)] = glyph
- def write(self, fp):
- self.header.write(fp)
- for n in range(0x20, 0x7F):
- self.glyphs[n].write(fp, False)
- class QFFUnicodeGlyphTableV1:
- type_id = 0x02
- def __init__(self):
- self.header = QGFBlockHeader()
- self.header.type_id = QFFUnicodeGlyphTableV1.type_id
- self.header.length = 0
-
- self.glyphs = {}
- def add_glyph(self, glyph: QFFGlyphInfo):
- self.glyphs[ord(glyph.code_point)] = glyph
- def write(self, fp):
- self.header.length = len(self.glyphs.keys()) * 6
- self.header.write(fp)
- for n in sorted(self.glyphs.keys()):
- self.glyphs[n].write(fp, True)
- class QFFFontDataDescriptorV1:
- type_id = 0x04
- def __init__(self):
- self.header = QGFBlockHeader()
- self.header.type_id = QFFFontDataDescriptorV1.type_id
- self.data = []
- def write(self, fp):
- self.header.length = len(self.data)
- self.header.write(fp)
- fp.write(bytes(self.data))
- def _generate_font_glyphs_list(use_ascii, unicode_glyphs):
-
- glyphs = {}
-
- if use_ascii is True:
- for c in range(0x20, 0x7F):
- glyphs[chr(c)] = True
-
- unicode_glyphs = list(unicode_glyphs)
- for c in unicode_glyphs:
- glyphs[c] = True
- return sorted(glyphs.keys())
- class QFFFont:
- def __init__(self, logger):
- self.logger = logger
- self.image = None
- self.glyph_data = {}
- self.glyph_height = 0
- return
- def _extract_glyphs(self, format):
- total_data_size = 0
- total_rle_data_size = 0
- converted_img = qmk.painter.convert_requested_format(self.image, format)
- (self.palette, _) = qmk.painter.convert_image_bytes(converted_img, format)
-
- for _, glyph_entry in self.glyph_data.items():
- glyph_img = converted_img.crop((glyph_entry.x, 1, glyph_entry.x + glyph_entry.w, 1 + self.glyph_height))
- (_, this_glyph_image_bytes) = qmk.painter.convert_image_bytes(glyph_img, format)
- this_glyph_rle_bytes = qmk.painter.compress_bytes_qmk_rle(this_glyph_image_bytes)
- total_data_size += len(this_glyph_image_bytes)
- total_rle_data_size += len(this_glyph_rle_bytes)
- glyph_entry['image_uncompressed_bytes'] = this_glyph_image_bytes
- glyph_entry['image_compressed_bytes'] = this_glyph_rle_bytes
- return (total_data_size, total_rle_data_size)
- def _parse_image(self, img, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
-
- self.image = None
-
- self.glyph_data = {}
- self.glyph_height = 0
-
- glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
-
- (width, height) = img.size
-
- glyph_pixel_offsets = []
- glyph_pixel_widths = []
- pixels = img.load()
-
- glyph_split_color = pixels[0, 0]
- glyph_pixel_offsets.append(0)
- last_offset = 0
- for x in range(1, width):
- if pixels[x, 0] == glyph_split_color:
- glyph_pixel_offsets.append(x)
- glyph_pixel_widths.append(x - last_offset)
- last_offset = x
- glyph_pixel_widths.append(width - last_offset)
-
- if len(glyph_pixel_offsets) != len(glyphs):
- self.logger.error('The number of glyphs to generate doesn\'t match the number of detected glyphs in the input image.')
- return
-
- for n in range(0, len(glyph_pixel_offsets)):
- self.glyph_data[glyphs[n]] = QFFGlyphInfo(code_point=glyphs[n], x=glyph_pixel_offsets[n], w=glyph_pixel_widths[n])
-
- self.image = img
- self.glyph_height = height - 1
- def generate_image(self, ttf_file: Path, font_size: int, include_ascii_glyphs: bool = True, unicode_glyphs: str = '', include_before_left: bool = False, use_aa: bool = True):
-
- font = ImageFont.truetype(str(ttf_file), int(font_size))
-
- max_font_size = font.font.ascent + abs(font.font.descent)
-
- glyphs = _generate_font_glyphs_list(include_ascii_glyphs, unicode_glyphs)
- baseline_offset = 9999999
- total_glyph_width = 0
- max_glyph_height = -1
-
- for glyph in glyphs:
- (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
- glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
- glyph_height = font.getbbox(glyph, anchor='la')[3]
- if max_glyph_height < glyph_height:
- max_glyph_height = glyph_height
- total_glyph_width += glyph_width
- if baseline_offset > ls_t:
- baseline_offset = ls_t
-
- img = Image.new("RGB", (total_glyph_width + 1, max_font_size * 2 + 1), (0, 0, 0, 255))
- cur_x_pos = 0
-
- for glyph in glyphs:
-
- (ls_l, ls_t, ls_r, ls_b) = font.getbbox(glyph, anchor='ls')
- glyph_width = (ls_r - ls_l) if include_before_left else (ls_r)
- glyph_height = ls_b - ls_t
- x_offset = -ls_l
- y_offset = ls_t - baseline_offset
-
- glyph_img = Image.new("RGB", (glyph_width, max_font_size), (0, 0, 0, 255))
- glyph_draw = ImageDraw.Draw(glyph_img)
- if not use_aa:
- glyph_draw.fontmode = "1"
- glyph_draw.text((x_offset, y_offset), glyph, font=font, anchor='lt')
-
- img.paste(glyph_img, (cur_x_pos, 1))
-
- pixels = img.load()
- pixels[cur_x_pos, 0] = (255, 0, 255)
-
- cur_x_pos += glyph_width
-
- pixels = img.load()
- pixels[cur_x_pos, 0] = (255, 0, 255)
-
- dummy_img = Image.new("RGB", (total_glyph_width + 1, max_font_size + 1), (0, 0, 0, 255))
- bbox = ImageChops.difference(img, dummy_img).getbbox()
- bbox = (bbox[0], bbox[1], bbox[2] - 1, bbox[3])
-
- self._parse_image(img.crop(bbox), include_ascii_glyphs, unicode_glyphs)
- def save_to_image(self, img_file: Path):
-
- if self.image is None:
- self.logger.error('No image is loaded.')
- return
-
- self.image.save(str(img_file))
- def read_from_image(self, img_file: Path, include_ascii_glyphs: bool = True, unicode_glyphs: str = ''):
-
- self._parse_image(Image.open(str(img_file)), include_ascii_glyphs, unicode_glyphs)
- return
- def save_to_qff(self, format: Dict[str, Any], use_rle: bool, fp):
-
- if self.image is None:
- self.logger.error('No image is loaded.')
- return
-
- (total_data_size, total_rle_data_size) = self._extract_glyphs(format)
- if use_rle:
- use_rle = (total_rle_data_size < total_data_size)
-
- img_buffer = bytes()
- for _, glyph_entry in self.glyph_data.items():
- glyph_entry['data_offset'] = len(img_buffer)
- glyph_img_bytes = glyph_entry.image_compressed_bytes if use_rle else glyph_entry.image_uncompressed_bytes
- img_buffer += bytes(glyph_img_bytes)
- font_descriptor = QFFFontDescriptor()
- ascii_table = QFFAsciiGlyphTableV1()
- unicode_table = QFFUnicodeGlyphTableV1()
- data_descriptor = QFFFontDataDescriptorV1()
- data_descriptor.data = img_buffer
-
- include_ascii_glyphs = all([chr(n) in self.glyph_data for n in range(0x20, 0x7F)])
-
- for code_point, glyph_entry in self.glyph_data.items():
- if ord(code_point) >= 0x20 and ord(code_point) <= 0x7E and include_ascii_glyphs:
- ascii_table.add_glyph(glyph_entry)
- else:
- unicode_table.add_glyph(glyph_entry)
-
- font_descriptor.line_height = self.glyph_height
- font_descriptor.has_ascii_table = include_ascii_glyphs
- font_descriptor.unicode_glyph_count = len(unicode_table.glyphs.keys())
- font_descriptor.is_transparent = False
- font_descriptor.format = format['image_format_byte']
- font_descriptor.compression = 0x01 if use_rle else 0x00
-
- font_descriptor_location = fp.tell()
- font_descriptor.write(fp)
-
- if font_descriptor.has_ascii_table:
- ascii_table.write(fp)
-
- if font_descriptor.unicode_glyph_count > 0:
- unicode_table.write(fp)
-
- if format['has_palette']:
- palette_descriptor = QGFFramePaletteDescriptorV1()
-
- 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))
-
- palette_descriptor.palette_entries = list(map(rgb888_to_qmk_hsv888, self.palette))
- palette_descriptor.write(fp)
-
- data_descriptor.write(fp)
-
- font_descriptor.total_file_size = fp.tell()
- fp.seek(font_descriptor_location, 0)
- font_descriptor.write(fp)
|