diff --git a/font_to_py.py b/font_to_py.py index d81b0ea..0a5a027 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -255,13 +255,11 @@ class Glyph(object): # height (in pixels) of all characters # width (in pixels) for monospaced output (advance width of widest char) class Font(dict): - def __init__(self, filename, size, minchar, maxchar, monospaced, defchar): + def __init__(self, filename, size, charset, monospaced): super().__init__() + self._glyphs = {} self._face = freetype.Face(filename) - if defchar is None: # Binary font - self.charset = [chr(char) for char in range(minchar, maxchar + 1)] - else: - self.charset = [chr(defchar)] + [chr(char) for char in range(minchar, maxchar + 1)] + self.charset = charset self.max_width = self.get_dimensions(size) self.width = self.max_width if monospaced else 0 for char in self.charset: # Populate dictionary @@ -274,31 +272,37 @@ class Font(dict): for npass in range(10): height += error self._face.set_pixel_sizes(0, height) + max_ascent = 0 max_descent = 0 - # For each character in the charset string we get the glyph - # and update the overall dimensions of the resulting bitmap. - max_width = 0 - max_ascent = 0 - for char in self.charset: + # for whatever wonderful reason, the fonts render differently if we only + # iterate over self.charset, so instead we use all of extended ASCII, cache + # the results, and cherry pick the ones we care about afterwards + for char in [chr(x) for x in range(32, 255)]: glyph = self._glyph_for_character(char) max_ascent = max(max_ascent, glyph.ascent) max_descent = max(max_descent, glyph.descent) - # for a few chars e.g. _ glyph.width > glyph.advance_width - max_width = int(max(max_width, glyph.advance_width, - glyph.width)) + self._glyphs[char] = {'glyph': glyph, + 'width': int(max(glyph.advance_width, glyph.width)), + 'ascent': glyph.ascent, + 'descent': glyph.descent} new_error = required_height - (max_ascent + max_descent) if (new_error == 0) or (abs(new_error) - abs(error) == 0): break error = new_error self.height = int(max_ascent + max_descent) + + max_width = 0 + for char in self.charset: + if self._glyphs[char]['width'] > max_width: + max_width = self._glyphs[char]['width'] + st = 'Height set in {} passes. Actual height {} pixels.\nMax character width {} pixels.' print(st.format(npass + 1, self.height, max_width)) self._max_descent = int(max_descent) return max_width - def _glyph_for_character(self, char): # Let FreeType load the glyph for the given character and tell it to # render a monochromatic bitmap representation. @@ -307,7 +311,7 @@ class Font(dict): return Glyph.from_glyphslot(self._face.glyph) def _render_char(self, char): - glyph = self._glyph_for_character(char) + glyph = self._glyphs[char]['glyph'] char_width = int(max(glyph.width, glyph.advance_width)) # Actual width width = self.width if self.width else char_width # Space required if monospaced outbuffer = Bitmap(width, self.height) @@ -347,57 +351,58 @@ class Font(dict): # PYTHON FILE WRITING STR01 = """# Code generated by font-to-py.py. -# Font: {} +# Font: %(font)s version = '0.2' +CHARSET = %(charset)s """ STR02 = """_mvfont = memoryview(_font) -def _chr_addr(ordch): - offset = 2 * (ordch - {}) - return int.from_bytes(_index[offset:offset + 2], 'little') +def _char_bounds(ch): + index = CHARSET[ch] + offset = 2 * index + start = int.from_bytes(_index[offset:offset+2], 'little') + next_offset = 2 * (index + 1) + end = int.from_bytes(_index[next_offset:next_offset+2], 'little') + return start, end -def get_ch(ch): - ordch = ord(ch) - ordch = ordch + 1 if ordch >= {} and ordch <= {} else {} - offset = _chr_addr(ordch) - width = int.from_bytes(_font[offset:offset + 2], 'little') - next_offs = _chr_addr(ordch +1) - return _mvfont[offset + 2:next_offs], {}, width - +def get_char(ch): + start, end = _char_bounds(ch) + width = int.from_bytes(_mvfont[start:start + 2], 'little') + return _mvfont[start + 2:end], %(height)s, width """ + def write_func(stream, name, arg): stream.write('def {}():\n return {}\n\n'.format(name, arg)) -# filename, size, minchar=32, maxchar=126, monospaced=False, defchar=ord('?'): -def write_font(op_path, font_path, height, monospaced, hmap, reverse, minchar, maxchar, defchar): +def write_font(op_path, font_path, height, monospaced, hmap, reverse, charset): try: - fnt = Font(font_path, height, minchar, maxchar, monospaced, defchar) + fnt = Font(font_path, height, charset, monospaced) except freetype.ft_errors.FT_Exception: print("Can't open", font_path) return False try: with open(op_path, 'w') as stream: - write_data(stream, fnt, font_path, monospaced, hmap, reverse, minchar, maxchar) + write_data(stream, fnt, font_path, monospaced, hmap, reverse, charset) except OSError: print("Can't open", op_path, 'for writing') return False return True -def write_data(stream, fnt, font_path, monospaced, hmap, reverse, minchar, maxchar): +def write_data(stream, fnt, font_path, monospaced, hmap, reverse, charset): height = fnt.height # Actual height, not target height - stream.write(STR01.format(os.path.split(font_path)[1])) + stream.write(STR01 % {'font': os.path.split(font_path)[1], + 'charset': {ch: i for i, ch in enumerate(charset)} + }) stream.write('\n') write_func(stream, 'height', height) write_func(stream, 'max_width', fnt.max_width) write_func(stream, 'hmap', hmap) write_func(stream, 'reverse', reverse) write_func(stream, 'monospaced', monospaced) - write_func(stream, 'min_ch', minchar) - write_func(stream, 'max_ch', maxchar) data, index = fnt.build_arrays(hmap, reverse) bw_font = ByteWriter(stream, '_font') bw_font.odata(data) @@ -405,7 +410,7 @@ def write_data(stream, fnt, font_path, monospaced, hmap, reverse, minchar, maxch bw_index = ByteWriter(stream, '_index') bw_index.odata(index) bw_index.eot() - stream.write(STR02.format(minchar, minchar, maxchar, minchar, height)) + stream.write(STR02 % {'height': height}) # BINARY OUTPUT # hmap reverse magic bytes @@ -474,22 +479,17 @@ if __name__ == "__main__": parser.add_argument('-s', '--smallest', type = int, - default = 32, - help = 'Ordinal value of smallest character default %(default)i') + help = 'Ordinal value of smallest character') parser.add_argument('-l', '--largest', type = int, - help = 'Ordinal value of largest character default %(default)i', - default = 126) + help = 'Ordinal value of largest character') - parser.add_argument('-e', '--errchar', - type = int, - help = 'Ordinal value of error character default %(default)i ("?")', - default = 63) + parser.add_argument('-c', '--charset', + help='List of characters to include in the generated font file', + default=[chr(x) for x in range(32, 127)]) args = parser.parse_args() - if not args.infile[0].isalpha(): - quit('Font filenames must be valid Python variable names.') if not os.path.isfile(args.infile): quit("Font filename does not exist") @@ -512,20 +512,14 @@ if __name__ == "__main__": if not os.path.splitext(args.outfile)[1].upper() == '.PY': quit('Output filename must have a .py extension.') - if args.smallest < 0: - quit('--smallest must be >= 0') - - if args.largest > 255: - quit('--largest must be < 256') - - if args.errchar < 0 or args.errchar > 255: - quit('--errchar must be between 0 and 255') + if args.smallest and args.largest: + charset = [chr(x) for x in range(args.smallest, args.largest)] + else: + charset = args.charset print('Writing Python font file.') if not write_font(args.outfile, args.infile, args.height, args.fixed, - args.xmap, args.reverse, args.smallest, args.largest, - args.errchar): + args.xmap, args.reverse, charset): sys.exit(1) print(args.outfile, 'written successfully.') -