#!/usr/bin/env python3 import bz2 import math import re import argparse CP_MAX = 0x10FFFF FONT_CPP = "data/font.bz2" FONT_HEIGHT = 12 class ReadBDFError(RuntimeError): def __init__(self, line_number, message): super().__init__(self, 'line %i: %s' % (line_number, message)) class FontTool: def __init__(self): with open(FONT_CPP, 'rb') as font_cpp: font_cpp_data = bz2.decompress(font_cpp.read()) i = 0 self.code_points = [False for _ in range(CP_MAX + 2)] while i < len(font_cpp_data): cp = font_cpp_data[i] | (font_cpp_data[i + 1] << 8) | (font_cpp_data[i + 2] << 16) width = font_cpp_data[i + 3] n = i + 4 + 3 * width self.code_points[cp] = font_cpp_data[(i + 3): n] i = n def commit(self): l = [] for i, data in enumerate(self.code_points): if data: l.append(i & 0xFF) l.append((i >> 8) & 0xFF) l.append((i >> 16) & 0xFF) l += data with open(FONT_CPP, 'wb') as font_cpp: font_cpp.write(bz2.compress(bytes(l))) def pack(cp_matrix): width = 0 for row in cp_matrix: if width < len(row): width = len(row) cp_data = [width] bits = 8 for row in cp_matrix: padded = row + [0] * (width - len(row)) for cv in padded: if bits == 8: cp_data.append(0) bits = 0 cp_data[-1] |= (cv & 3) << bits bits += 2 return cp_data def unpack(cp_data): ptr = 1 bits = 0 buf = 0 cp_matrix = [] for y in range(FONT_HEIGHT): cp_matrix.append([]) for x in range(cp_data[0]): if bits == 0: buf = cp_data[ptr] ptr += 1 bits = 8 cp_matrix[-1].append(buf & 3) buf >>= 2 bits -= 2 return cp_matrix class RawReader: def __init__(self, path): self.code_points = [False for _ in range(CP_MAX + 2)] with open(path) as raw: items = [int(v) for v in re.findall(r'[0-9]+', raw.read())] ptr = 0 while ptr <= len(items) - 2: cp = items[ptr] width = items[ptr + 1] ptr += 2 matrix = [] for i in range(ptr, ptr + width * FONT_HEIGHT, width): matrix.append(items[i: (i + width)]) ptr += width * FONT_HEIGHT self.code_points[cp] = FontTool.pack(matrix) class BDFReader: def __init__(self, path, xoffs, yoffs): self.code_points = [False for _ in range(CP_MAX + 2)] item_re = re.compile(r'[^ \n\r]+') with open(path) as bdf: global_dw = False startchar = False bitmap = False char_dw = False char_cp = False char_bbx = False skip = 0 for line_number, line in enumerate(bdf): if skip: skip -= 1 continue items = re.findall(item_re, line) if startchar and items[0] == 'ENDCHAR': if len(bitmap) != char_bbx[1]: raise ReadBDFError(line_number, "invalid bitmap data") cp_matrix = [] for y in range(FONT_HEIGHT): cp_matrix.append([]) for x in range(char_dw): cv = 0 xx = x + xoffs yy = FONT_HEIGHT - 1 - y + yoffs if char_bbx[2] <= xx < char_bbx[0] + char_bbx[2] and char_bbx[3] <= yy < char_bbx[1] + \ char_bbx[3]: cv = bitmap[char_bbx[1] - 1 - (yy - char_bbx[3])][xx - char_bbx[2]] * 3 cp_matrix[-1].append(cv) self.code_points[char_cp] = FontTool.pack(cp_matrix) startchar = False bitmap = False char_dw = False char_cp = False char_bbx = False elif bitmap != False: if len(items) != 1: raise ReadBDFError(line_number, "missing bitmap data") bits = [] for ch in items[0]: cv = int(ch, 16) bits += [cv & 8 and 1 or 0, cv & 4 and 1 or 0, cv & 2 and 1 or 0, cv & 1 and 1 or 0] bitmap.append(bits[: char_bbx[0]]) elif items[0] == 'SIZE': if len(items) != 4: raise ReadBDFError(line_number, "invalid directive") elif items[0] == 'FONTBOUNDINGBOX': if len(items) != 5: raise ReadBDFError(line_number, "invalid directive") elif not startchar and items[0] == 'STARTCHAR': startchar = True char_dw = global_dw elif items[0] == 'STARTPROPERTIES': if len(items) != 2: raise ReadBDFError(line_number, "invalid directive") skip = int(items[1]) + 1 elif startchar and items[0] == 'BITMAP': bitmap = [] elif startchar and items[0] == 'BBX': if len(items) != 5: raise ReadBDFError(line_number, "invalid directive") char_bbx = [int(items[1]), int(items[2]), int(items[3]), int(items[4])] elif startchar and items[0] == 'ENCODING': if len(items) != 2: raise ReadBDFError(line_number, "invalid directive") char_cp = int(items[1]) elif items[0] == 'METRICSSET': if len(items) != 2: raise ReadBDFError(line_number, "invalid directive") if int(items[1]) == 1: raise ReadBDFError(line_number, "font does not support writing direction 0") elif items[0] == 'DWIDTH': if len(items) != 3: raise ReadBDFError(line_number, "invalid directive") if int(items[2]) != 0: raise ReadBDFError(line_number, "vertical component of dwidth vector is non-zero") char_dw = int(items[1]) if not startchar: global_dw = char_dw if __name__ == "__main__": parser = argparse.ArgumentParser("fonttool.py", description="font tools for managing fonts, this script can be" " imported as a module", fromfile_prefix_chars="@") command = parser.add_subparsers(dest="command", required=True) addbdf = command.add_parser("addbdf", help="Adds BDF Formated Font") addbdf.add_argument("first", metavar="FIRST", type=int) addbdf.add_argument("last", metavar="LAST", type=int) addbdf.add_argument("bdffile", metavar="BDFFILE", help="BDF is an archaic bitmap font format") addbdf.add_argument("xoffs", metavar="XOFFS", nargs="?", default=0, type=int, help="Defaults to 0") addbdf.add_argument("yoffs", metavar="YOFFS", nargs="?", default=0, type=int, help="Defaults to 0") addraw = command.add_parser("addraw", help="Adds a Raw Formated Font") addraw.add_argument("first", metavar="FIRST", type=int) addraw.add_argument("last", metavar="LAST", type=int) addraw.add_argument("rawfile", metavar="RAWFILE", help=""""Raw" files are simply ASCII-encoded white-space delimited \ lists of decimal integer constants. These lists of integers encode characters as any number of consecutive character description structures laid out as follows: * the code point corresponding to the character being described; * the width in pixels of the character being described; * width times %i brightness levels between 0 and 3, a row-major matrix.""") remove = command.add_parser("remove", help="Remove") remove.add_argument("first", metavar="FIRST", type=int) remove.add_argument("last", metavar="LAST", type=int, default=None, nargs="?", help="Defaults to FIRST") copy = command.add_parser("copy", help="Copy") copy.add_argument("dest", metavar="DSTFIRST", type=int) copy.add_argument("first", metavar="SRCFIRST", type=int) copy.add_argument("last", metavar="SRCLAST", type=int, default=None, nargs="?", help="Defaults to SRCFIRST") inspect = command.add_parser("inspect", help="Inspect") inspect.add_argument("first", metavar="FIRST", type=int) inspect.add_argument("last", metavar="LAST", type=int, default=None, nargs="?", help="Defaults to FIRST") args = parser.parse_args() cp_first = args.first if args.last is None: cp_last = cp_first else: cp_last = args.last if cp_first < 0 or cp_last > CP_MAX or cp_first > cp_last: print('invalid range') exit(1) ft = FontTool() if args.command == "addbdf": xoffs = args.xoffs yoffs = args.yoffs bdfr = BDFReader(args.bdffile, xoffs, yoffs) for i in range(cp_first, cp_last + 1): if bdfr.code_points[i] and not ft.code_points[i]: ft.code_points[i] = bdfr.code_points[i] ft.commit() elif args.command == 'addraw': rr = RawReader(args.rawfile) for i in range(cp_first, cp_last + 1): if rr.code_points[i] and not ft.code_points[i]: ft.code_points[i] = rr.code_points[i] ft.commit() elif args.command == 'remove': for i in range(cp_first, cp_last + 1): ft.code_points[i] = False ft.commit() elif args.command == 'copy': for i in range(cp_first, cp_last + 1): ft.code_points[i + (args.dest - cp_first)] = ft.code_points[i] ft.commit() elif args.command == 'inspect': lut = [' ', '░░', '▒▒', '▓▓'] for i in range(cp_first, cp_last + 1): if ft.code_points[i]: print('code point %i (%c)' % (i, i)) print('') print('\n'.join([''.join([lut[ch] for ch in row]) for row in FontTool.unpack(ft.code_points[i])])) print('') else: print('code point %i (%c) is not available' % (i, i))