diff --git a/FONT_TO_PY.md b/FONT_TO_PY.md index 562efff..c16ff50 100644 --- a/FONT_TO_PY.md +++ b/FONT_TO_PY.md @@ -23,7 +23,8 @@ Example usage to produce a file ``myfont.py`` with height of 23 pixels: 1. Font file path. Must be a ttf or otf file. 2. Height in pixels. - 3. Output file path. Must have a .py extension. + 3. Output file path. Must have a .py extension otherwise a binary font file + will be created; in this instance a warning message is output. ### Optional arguments: @@ -49,6 +50,16 @@ to render strings on demand. A practical example may be studied [here](https://github.com/peterhinch/micropython-samples/blob/master/SSD1306/ssd1306_test.py). The detailed layout of the Python file may be seen [here](./DRIVERS.md). +### Binary font files + +If the output filename does not have a ``.py`` extension a binary font file is +created. This is primarily intended for the e-paper driver. Specifically in +applications where the file is to be stored on the display's internal flash +memory rather than using frozen Python modules. + +The technique of accessing character data from a random access file is only +applicable to devices such as e-paper where the update time is slow. + # Dependencies, links and licence The code is released under the MIT licence. It requires Python 3.2 or later. diff --git a/font_to_py.py b/font_to_py.py index 1d88177..2a9219a 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -2,6 +2,8 @@ # -*- coding: utf-8 -*- # Needs freetype-py>=1.0 +# Implements multi-pass solution to setting an exact font height + # Some code adapted from Daniel Bader's work at the following URL # http://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python @@ -258,26 +260,41 @@ class Font(dict): def __init__(self, filename, size, monospaced=False): super().__init__() self._face = freetype.Face(filename) - self._face.set_pixel_sizes(0, size) - self._max_descent = 0 - - # For each character in the charset string we get the glyph - # and update the overall dimensions of the resulting bitmap. - self.max_width = 0 - max_ascent = 0 - for char in self.charset: - glyph = self._glyph_for_character(char) - max_ascent = max(max_ascent, int(glyph.ascent)) - self._max_descent = max(self._max_descent, int(glyph.descent)) - # for a few chars e.g. _ glyph.width > glyph.advance_width - self.max_width = int(max(self.max_width, glyph.advance_width, - glyph.width)) - - self.height = max_ascent + self._max_descent + self.max_width = self.get_dimensions(size) self.width = self.max_width if monospaced else 0 - for char in self.charset: + for char in self.charset: # Populate dictionary self._render_char(char) + # n-pass solution to setting a precise height. + def get_dimensions(self, required_height): + error = 0 + height = required_height + for npass in range(10): + height += error + self._face.set_pixel_sizes(0, height) + 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: + glyph = self._glyph_for_character(char) + max_ascent = max(max_ascent, int(glyph.ascent)) + max_descent = max(max_descent, int(glyph.descent)) + # for a few chars e.g. _ glyph.width > glyph.advance_width + max_width = int(max(max_width, glyph.advance_width, + glyph.width)) + + error = required_height - (max_ascent + max_descent) + if error == 0: + break + print('Height set in {} passes'.format(npass)) + self.height = max_ascent + max_descent + self._max_descent = 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. @@ -287,20 +304,18 @@ class Font(dict): def _render_char(self, char): glyph = self._glyph_for_character(char) - if self.width: # Monospaced - width = self.width - else: - width = int(max(glyph.width, glyph.advance_width)) + 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) # The vertical drawing position should place the glyph # on the baseline as intended. row = self.height - int(glyph.ascent) - self._max_descent outbuffer.bitblt(glyph.bitmap, row) - self[char] = [outbuffer, width] + self[char] = [outbuffer, width, char_width] def stream_char(self, char, hmap, reverse): - outbuffer, _ = self[char] + outbuffer, _, _ = self[char] if hmap: gen = outbuffer.get_hbyte(reverse) else: @@ -317,6 +332,14 @@ class Font(dict): index += (len(data)).to_bytes(2, byteorder='little') return data, index + def build_binary_array(self, hmap, reverse): + data = bytearray((0x3f, 0xe7, self.max_width, self.height)) + for char in self.charset: + width = self[char][2] + data += bytes((width,)) + data += bytearray(self.stream_char(char, hmap, reverse)) + return data + # PYTHON FILE WRITING STR01 = """# Code generated by font-to-py.py. @@ -377,6 +400,22 @@ def write_data(stream, fnt, font_path, monospaced, hmap, reverse): bw_index.eot() stream.write(STR02.format(height, height)) +# BINARY OUTPUT + +def write_binary_font(op_path, font_path, height, hmap, reverse): + try: + fnt = Font(font_path, height, True) # All chars have same width + except freetype.ft_errors.FT_Exception: + print("Can't open", font_path) + return False + try: + with open(op_path, 'wb') as stream: + data = fnt.build_binary_array(hmap, reverse) + stream.write(data) + except OSError: + print("Can't open", op_path, 'for writing') + return False + return True # PARSE COMMAND LINE ARGUMENTS @@ -412,10 +451,13 @@ if __name__ == "__main__": if not os.path.splitext(args.infile)[1].upper() in ('.TTF', '.OTF'): print("Font file should be a ttf or otf file.") sys.exit(1) - if not os.path.splitext(args.outfile)[1].upper() == '.PY': - print("Output filename should have a .py extension.") - sys.exit(1) - if not write_font(args.outfile, args.infile, args.height, args.fixed, - args.xmap, args.reverse): - sys.exit(1) + if os.path.splitext(args.outfile)[1].upper() == '.PY': # Emit Python + if not write_font(args.outfile, args.infile, args.height, args.fixed, + args.xmap, args.reverse): + sys.exit(1) + else: + print('WARNING: output filename lacks .py extension. Writing binary font file.') + if not write_binary_font(args.outfile, args.infile, args.height, + args.xmap, args.reverse): + sys.exit(1) print(args.outfile, 'written successfully.')