From 0de8876e672e88751dd7795ca13263c754b3a3be Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Sat, 24 Jun 2017 09:00:48 -0400 Subject: [PATCH 01/16] update font_to_py to support non-sequential charsets --- font_to_py.py | 108 ++++++++++++++++++++++++-------------------------- 1 file changed, 51 insertions(+), 57 deletions(-) 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.') - From 85f90b0900e8ba6dcc02f86c7fe60c63d48c047e Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Sat, 24 Jun 2017 09:06:54 -0400 Subject: [PATCH 02/16] update writer to support rotation and be ~3x faster --- DRIVERS.md | 20 +++---- FONT_TO_PY.md | 2 +- README.md | 8 +-- driver_test.py | 6 +- font_test.py | 4 +- writer.py | 157 +++++++++++++++++++++++++++++++++---------------- 6 files changed, 126 insertions(+), 71 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index 335c37d..3a99cc4 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -18,9 +18,9 @@ display = Display(args_required_by_driver) wri_serif = Writer(display, freeserif) wri_sans = Writer(display, freesans20) Writer.set_clip(True, True) -wri_serif.printstring('Tuesday\n') -wri_sans.printstring('8 Nov 2016\n') -wri_sans.printstring('10.30am') +wri_serif.draw_text('Tuesday\n') +wri_sans.draw_text('8 Nov 2016\n') +wri_sans.draw_text('10.30am') display.show() # Display the result ``` @@ -33,10 +33,10 @@ display and a complete example of an SSD1306 driver may be found The ``Writer`` class exposes the following class methods: - 1. ``set_textpos`` Args: ``row``, ``col``. This determines where on screen any + 1. ``set_position`` Args: ``x``, ``y``. This determines where on screen any subsequent text is to be rendered. The initial value is (0, 0) - the top left - corner. Arguments are in pixels with positive values representing down and - right respectively. They reference the top left hand corner of the first + corner. Arguments are in pixels with positive values representing right and + down respectively. They reference the top left hand corner of the first character to be output. 2. ``set_clip`` Args: boolean ``row_clip``, ``col_clip``. If these are ``True``, characters will be clipped if they extend beyond the boundaries of @@ -49,7 +49,7 @@ of characters is maintained regardless of the font in use. ## Method - 1. ``printstring`` Arg: a text string. Outputs a text string at the current + 1. ``draw_text`` Arg: a text string. Outputs a text string at the current insertion point. Newline characters are honoured. ## Note on the Writer class @@ -110,7 +110,7 @@ buffer/device. ### Specifying the font file -Each font file has a ``get_ch()`` function accepting an ASCII character as its +Each font file has a ``get_char()`` function accepting an ASCII character as its argument. It returns a memoryview instance providing access to a bytearray corresponding to the individual glyph. The layout of this data is determined by the command line arguments presented to the ``font_to_py.py`` utility. It is @@ -171,7 +171,7 @@ b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\ _mvfont = memoryview(_font) # Boilerplate code omitted -def get_ch(ch): +def get_char(ch): # validate ch, if out of range use '?' # get offsets into _font and retrieve char width # Return: memoryview of bitmap, height and width @@ -187,7 +187,7 @@ will fit the available space. If it will fit on the assumption that all chars are maximum width, it can be rendered rapidly without doing a character by character check. -``get_ch()`` returns a memoryview of an individual glyph with its dimensions +``get_char()`` returns a memoryview of an individual glyph with its dimensions and contains all the bytes required to render the character including trailing space. diff --git a/FONT_TO_PY.md b/FONT_TO_PY.md index 6fdecd8..8d7689a 100644 --- a/FONT_TO_PY.md +++ b/FONT_TO_PY.md @@ -114,7 +114,7 @@ gc.collect() micropython.mem_info() def foo(): - addr, height, width = freeserif.get_ch('a') + addr, height, width = freeserif.get_char('a') foo() diff --git a/README.md b/README.md index e78724f..1ab17ec 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ all pixels of a character are rendered identically. Converting font files programmatically works best for larger fonts. For small fonts, like the 8*8 default used by the SSD1306 driver, it is best to use -hand-designed binary font files: these are optiised for rendering at a specific +hand-designed binary font files: these are optimised for rendering at a specific size. # Font file interface @@ -79,13 +79,11 @@ provided to font-to-py: ``hmap`` Returns ``True`` if font is horizontally mapped. Should return ``True`` ``reverse`` Returns ``True`` if bit reversal was specified. Should return ``False`` ``monospaced`` Returns ``True`` if monospaced rendering was specified. -``min_ch`` Returns the ordinal value of the lowest character in the file. -``max_ch`` Returns the ordinal value of the highest character in the file. -Glyphs are returned with the ``get_ch`` method. Its argument is a character +Glyphs are returned with the ``get_char`` method. Its argument is a character and it returns the following values: - * A ``memoryview`` object containg the glyph bytes. + * A ``memoryview`` object containing the glyph bytes. * The height in pixels. * The character width in pixels. diff --git a/driver_test.py b/driver_test.py index 04a6ec4..34c33b6 100644 --- a/driver_test.py +++ b/driver_test.py @@ -71,9 +71,9 @@ else: # I2C serif = Writer(display, freeserif) sans = Writer(display, freesans20) Writer.set_clip(True, True) # Disable auto scrolling and wrapping. -serif.printstring('Tuesday\n') -sans.printstring('8 Nov 2016\n') -sans.printstring('10.30am') +serif.draw_text('Tuesday\n') +sans.draw_text('8 Nov 2016\n') +sans.draw_text('10.30am') display.show() diff --git a/font_test.py b/font_test.py index 645fcf8..ef96d0f 100644 --- a/font_test.py +++ b/font_test.py @@ -143,7 +143,7 @@ def test_font(fontfile, string): height = myfont.height() for row in range(height): for char in string: - data, _, width = myfont.get_ch(char) + data, _, width = myfont.get_char(char) if myfont.hmap(): render_row_hmap(data, row, height, width, myfont.reverse()) else: @@ -167,7 +167,7 @@ def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord( height = myfont.height() for row in range(height): for char in string: - data, _, width = myfont.get_ch(char) + data, _, width = myfont.get_char(char) if myfont.hmap(): render_row_hmap(data, row, height, width, myfont.reverse()) else: diff --git a/writer.py b/writer.py index 2a03c82..c1219ba 100644 --- a/writer.py +++ b/writer.py @@ -29,71 +29,128 @@ class Writer(object): - text_row = 0 # attributes common to all Writer instances - text_col = 0 - row_clip = False # Clip or scroll when screen full - col_clip = False # Clip or new line when row is full + # these attributes and set_position are common to all Writer instances + x_pos = 0 + y_pos = 0 + device = None + screen_height = 0 + screen_width = 0 + draw_pixel = None @classmethod - def set_textpos(cls, row, col): - cls.text_row = row - cls.text_col = col + def set_position(cls, x, y): + cls.x_pos = x + cls.y_pos = y - @classmethod - def set_clip(cls, row_clip, col_clip): - cls.row_clip = row_clip - cls.col_clip = col_clip - - def __init__(self, device, font): + def __init__(self, device, font, rotation=None): super().__init__() self.device = device + self.set_font(font) + self.set_rotation(rotation) + + def set_font(self, font): self.font = font + self._draw_char = self._draw_vmap_char if font.hmap(): - raise OSError('Font must be vertically mapped') - self.screenwidth = device.width # In pixels - self.screenheight = device.height + self._draw_char = self._draw_hmap_char + + @classmethod + def set_rotation(cls, rotation=None): + rotation = 0 if not rotation else rotation % 360 + if not rotation: + cls.draw_pixel = cls._draw_pixel + elif rotation == 90: + cls.draw_pixel = cls._draw_pixel_90 + elif rotation == 180: + cls.draw_pixel = cls._draw_pixel_180 + elif rotation == 270: + cls.draw_pixel = cls._draw_pixel_270 + else: + raise ValueError('rotation must be falsy or one of 90, 180 or 270') + + if not rotation or rotation == 180: + cls.screen_width = cls.device.width + cls.screen_height = cls.device.height + else: + cls.screen_width = cls.device.height + cls.screen_height = cls.device.width + + def _draw_pixel(self, x, y, color): + self.device.pixel(x, y, color) + + def _draw_pixel_90(self, x, y, color): + self.device.pixel(self.device.width - y, x, color) + + def _draw_pixel_180(self, x, y, color): + self.device.pixel(self.device.width - x, self.device.height - y, color) + + def _draw_pixel_270(self, x, y, color): + self.device.pixel(y, self.device.height - x, color) def _newline(self): - height = self.font.height() - Writer.text_row += height - Writer.text_col = 0 - margin = self.screenheight - (Writer.text_row + height) - if margin < 0: - if not Writer.row_clip: - self.device.scroll(0, margin) - Writer.text_row += margin + Writer.x_pos = 0 + Writer.y_pos += self.font.height() - def printstring(self, string): + def draw_text(self, string): for char in string: - self._printchar(char) + self._draw_char(char) - def _printchar(self, char): + def _draw_hmap_char(self, char): if char == '\n': self._newline() return - glyph, char_height, char_width = self.font.get_ch(char) - if Writer.text_row + char_height > self.screenheight: - if Writer.row_clip: - return + + glyph, char_height, char_width = self.font.get_char(char) + + if Writer.x_pos + char_width > self.screen_width: + self._newline() + + div, mod = divmod(char_width, 8) + bytes_per_row = div + 1 if mod else div + + for glyph_row_i in range(char_height): + glyph_row_start = glyph_row_i * bytes_per_row + glyph_row = int.from_bytes( + glyph[glyph_row_start:glyph_row_start + bytes_per_row], + 'little' + ) + if not glyph_row: + continue + x = Writer.x_pos + y = Writer.y_pos + glyph_row_i + for glyph_col_i in range(char_width): + if glyph_row & (1 << glyph_col_i): + self.draw_pixel(x, y, 1) + x += 1 + + Writer.x_pos += char_width + + def _draw_vmap_char(self, char): + if char == '\n': + self._newline() + return + + glyph, char_height, char_width = self.font.get_char(char) + + if Writer.x_pos + char_width > self.screen_width: self._newline() - if Writer.text_col + char_width > self.screenwidth: - if Writer.col_clip: - return - else: - self._newline() div, mod = divmod(char_height, 8) - gbytes = div + 1 if mod else div # No. of bytes per column of glyph - device = self.device - for scol in range(char_width): # Source column - dcol = scol + Writer.text_col # Destination column - drow = Writer.text_row # Destination row - for srow in range(char_height): # Source row - gbyte, gbit = divmod(srow, 8) - if drow >= self.screenheight: - break - if gbit == 0: # Next glyph byte - data = glyph[scol * gbytes + gbyte] - device.pixel(dcol, drow, data & (1 << gbit)) - drow += 1 - Writer.text_col += char_width + bytes_per_col = div + 1 if mod else div + + for glyph_col_i in range(char_width): + glyph_col_start = glyph_col_i * bytes_per_col + glyph_col = int.from_bytes( + glyph[glyph_col_start:glyph_col_start + bytes_per_col], + 'little' + ) + if not glyph_col: + continue + x = Writer.x_pos + glyph_col_i + y = Writer.y_pos + for glyph_row_i in range(char_height): + if glyph_col & (1 << glyph_row_i): + self.draw_pixel(x, y, 1) + y += 1 + + Writer.x_pos += char_width From 2ef0403ed4e94023db96b9a6c41a9a5748124715 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Sun, 25 Jun 2017 19:50:04 -0400 Subject: [PATCH 03/16] add font line mapping format + rendering code --- display.py | 96 ++++++++++++++++ display_test.py | 34 ++++++ font_to_py.py | 292 ++++++++++++++++++++++++++++++++++++++++-------- writer.py | 154 ++++++++++++++----------- 4 files changed, 461 insertions(+), 115 deletions(-) create mode 100644 display.py create mode 100644 display_test.py diff --git a/display.py b/display.py new file mode 100644 index 0000000..bce157d --- /dev/null +++ b/display.py @@ -0,0 +1,96 @@ +import ssd1306 + +from writer import Writer + + +class Display(ssd1306.SSD1306_I2C): + screen_height = 0 + screen_width = 0 + + def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False, + default_font=None, rotation=None): + super().__init__(width, height, i2c, addr, external_vcc) + self.set_default_font(default_font) + self.set_rotation(rotation) + + def set_default_font(self, default_font): + self.default_font = default_font + + def set_position(self, x, y): + Writer.set_position(x, y) + + def draw_text(self, text, x=None, y=None, font=None): + if x is not None and y is not None: + Writer.set_position(x, y) + if font: + font.draw_text(text) + else: + self.default_font.draw_text(font) + + def clear(self): + self.fill(0) + self.show() + + def set_rotation(self, rotation=None): + rotation = 0 if not rotation else rotation % 360 + if not rotation: + self.pixel = self._pixel + self.hline = self._hline + self.vline = self._vline + elif rotation == 90: + self.pixel = self._pixel_90 + self.hline = self._hline_90 + self.vline = self._vline_90 + elif rotation == 180: + self.pixel = self._pixel_180 + self.hline = self._hline_180 + self.vline = self._vline_180 + elif rotation == 270: + self.pixel = self._pixel_270 + self.hline = self._hline_270 + self.vline = self._vline_270 + else: + raise ValueError('rotation must be falsy or one of 90, 180 or 270') + + if not rotation or rotation == 180: + self.screen_width = self.width + self.screen_height = self.height + else: + self.screen_width = self.height + self.screen_height = self.width + + def _pixel(self, x, y, color=1): + self.framebuf.pixel(x, y, color) + + def _hline(self, x, y, length, color=1): + self.framebuf.hline(x, y, length, color) + + def _vline(self, x, y, length, color=1): + self.framebuf.vline(x, y, length, color) + + def _pixel_90(self, x, y, color=1): + self.framebuf.pixel(self.width - y, x, color) + + def _hline_90(self, x, y, length, color=1): + self.framebuf.vline(self.width - y - 1, x, length, color) + + def _vline_90(self, x, y, length, color=1): + self.framebuf.hline(self.width - y - length, x, length, color) + + def _pixel_180(self, x, y, color=1): + self.framebuf.pixel(self.width - x, self.height - y, color) + + def _hline_180(self, x, y, length, color=1): + self.framebuf.hline(self.width - x - length, self.height - y - 1, length, color) + + def _vline_180(self, x, y, length, color=1): + self.framebuf.vline(self.width - x - 1, self.height - y - length, length, color) + + def _pixel_270(self, x, y, color=1): + self.framebuf.pixel(y, self.height - x, color) + + def _hline_270(self, x, y, length, color=1): + self.framebuf.vline(y, self.height - x - length, length, color) + + def _vline_270(self, x, y, length, color=1): + self.framebuf.hline(y, self.height - x - 1, length, color) diff --git a/display_test.py b/display_test.py new file mode 100644 index 0000000..5039fa4 --- /dev/null +++ b/display_test.py @@ -0,0 +1,34 @@ +import gc +import machine +import utime + +from display import Display +from writer import Writer + +import DejaVuSans24_l + + +i2c = machine.I2C(sda=machine.Pin(5), scl=machine.Pin(4)) +display = Display(128, 64, i2c) +sans24 = Writer(display, DejaVuSans24_l) +display.set_default_font(sans24) + +rotation = 0 +while True: + start = utime.ticks_us() + display.clear() + display.set_position(0, 0) + display.set_rotation(rotation) + display.draw_text('abcdefghijklmnopqrstuvwxyz') + # display.hline(0, 0, display.screen_width) + # display.hline(0, display.screen_height-1, display.screen_width) + # display.vline(0, 0, display.screen_height) + # display.vline(display.screen_width-1, 0, display.screen_height) + + display.show() + end = utime.ticks_us() + print("time: %0.2fms" % ((end - start) / 1e3)) + gc.collect() + print("memory:", gc.mem_alloc()) + utime.sleep(5) + rotation += 90 diff --git a/font_to_py.py b/font_to_py.py index 0a5a027..4c05f07 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -33,6 +33,7 @@ import argparse import sys import os import freetype +import itertools # UTILITIES FOR WRITING PYTHON SOURCECODE TO A FILE @@ -43,6 +44,10 @@ import freetype # Lines are broken with \ for readability. +def flatten(l): + return list(itertools.chain.from_iterable(l)) + + class ByteWriter(object): bytes_per_line = 16 @@ -89,26 +94,65 @@ def var_write(stream, name, value): # FONT HANDLING +def byte(data, signed=False): + return data.to_bytes(1, byteorder='little', signed=signed) + + +def byte_pair(data, signed=False): + return data.to_bytes(2, byteorder='little', signed=signed) + + class Bitmap(object): """ A 2D bitmap image represented as a list of byte values. Each byte indicates the state of a single pixel in the bitmap. A value of 0 indicates that the pixel is `off` and any other value indicates that it is `on`. """ - def __init__(self, width, height, pixels=None): + def __init__(self, char, width, height, pixels=None): + self.char = char self.width = width self.height = height self.pixels = pixels or bytearray(width * height) + self.lh_data = [] + self.lv_data = [] def display(self): """Print the bitmap's pixels.""" - for row in range(self.height): - for col in range(self.width): - char = '#' if self.pixels[row * self.width + col] else '.' + lh_count = len(flatten(self.lh_data)) + print('{} horizontal line mapping: {} hline draw calls. {} bytes'.format( + self.char, + lh_count, + len(list(self._stream_lhmap())) + )) + print('v' * len(''.join([str(i) for i in range(self.width)])), ' y [(x, length)]') + for y in range(self.height): + for x in range(self.width): + space = ' ' if x < 10 else ' ' + char = space if self.pixels[y * self.width + x] else x print(char, end='') - print() + print(' ', '%2d' % y, self.lh_data[y]) print() + lv_count = len(flatten(self.lv_data)) + print('{} vertical line mapping: {} vline draw calls. {} bytes'.format( + self.char, + lv_count, + len(list(self._stream_lvmap())) + )) + print('>' * len(''.join([str(i) for i in range(self.height)])), ' x [(y, length)]') + for x in range(self.width)[::-1]: + for y in range(self.height): + space = ' ' if y < 10 else ' ' + char = space if self.pixels[y * self.width + x] else y + print(char, end='') + print(' ', '%2d' % x, self.lv_data[x]) + print() + + print('selecting {} mapping for {} char\n'.format( + 'lhmap horizontal' if self.is_char_lhmap() else 'lvmap vertical', + self.char + )) + def bitblt(self, src, row): """Copy all pixels from `src` into this bitmap""" srcpixel = 0 @@ -122,50 +166,135 @@ class Bitmap(object): dstpixel += 1 dstpixel += row_offset + # calc horizontal line mapping + for y in range(self.height): + self.lh_data.append([]) + x = 0 + while x < self.width: + if self.pixels[y * self.width + x]: + line_start = x + line_end = x + inline_x = x + while inline_x <= self.width: + if inline_x < self.width and self.pixels[y * self.width + inline_x]: + inline_x += 1 + else: + line_end = inline_x + break + self.lh_data[y].append((line_start, line_end - line_start)) + x = line_end + 1 + else: + x += 1 + + # calc vertical line mapping + for x in range(self.width): + self.lv_data.append([]) + y = 0 + while y < self.height: + if self.pixels[y * self.width + x]: + line_start = y + line_end = y + inline_y = y + while inline_y <= self.height: + if inline_y < self.height and self.pixels[inline_y * self.width + x]: + inline_y += 1 + else: + line_end = inline_y + break + self.lv_data[x].append((line_start, line_end - line_start)) + y = line_end + 1 + else: + y += 1 + + def is_char_lhmap(self): + len_lhmap = len(flatten(self.lh_data)) + len_lvmap = len(flatten(self.lv_data)) + if len_lhmap == len_lvmap: + return len(list(self._stream_lhmap())) <= len(list(self._stream_lvmap())) + return len_lhmap <= len_lvmap + + def stream(self): + if self.is_char_lhmap(): + yield from self._stream_lhmap() + else: + yield from self._stream_lvmap() + + def _stream_lhmap(self): + prev_row = None + for y, row in enumerate(self.lh_data): + if not row: + prev_row = None + continue + elif row == prev_row: + yield byte(0) + else: + yield byte(len(row)) + yield byte(y) + for x, length in row: + yield byte(x) + yield byte(length) + prev_row = row + + def _stream_lvmap(self): + prev_col = None + for x, col in enumerate(self.lv_data): + if not col: + prev_col = None + continue + elif col == prev_col: + yield byte(0) + else: + yield byte(len(col)) + yield byte(x) + for y, length in col: + yield byte(y) + yield byte(length) + prev_col = col + # Horizontal mapping generator function def get_hbyte(self, reverse): - for row in range(self.height): - col = 0 + for y in range(self.height): + x = 0 while True: - bit = col % 8 + bit = x % 8 if bit == 0: - if col >= self.width: + if x >= self.width: break byte = 0 - if col < self.width: + if x < self.width: if reverse: - byte |= self.pixels[row * self.width + col] << bit + byte |= self.pixels[y * self.width + x] << bit else: # Normal map MSB of byte 0 is (0, 0) - byte |= self.pixels[row * self.width + col] << (7 - bit) + byte |= self.pixels[y * self.width + x] << (7 - bit) if bit == 7: yield byte - col += 1 + x += 1 # Vertical mapping def get_vbyte(self, reverse): - for col in range(self.width): - row = 0 + for x in range(self.width): + y = 0 while True: - bit = row % 8 + bit = y % 8 if bit == 0: - if row >= self.height: + if y >= self.height: break byte = 0 - if row < self.height: + if y < self.height: if reverse: - byte |= self.pixels[row * self.width + col] << (7 - bit) + byte |= self.pixels[y * self.width + x] << (7 - bit) else: # Normal map MSB of byte 0 is (0, 7) - byte |= self.pixels[row * self.width + col] << bit + byte |= self.pixels[y * self.width + x] << bit if bit == 7: yield byte - row += 1 + y += 1 class Glyph(object): - def __init__(self, pixels, width, height, top, advance_width): - self.bitmap = Bitmap(width, height, pixels) + def __init__(self, char, pixels, width, height, top, advance_width): + self.bitmap = Bitmap(char, width, height, pixels) # The glyph bitmap's top-side bearing, i.e. the vertical distance from # the baseline to the bitmap's top-most scanline. @@ -190,7 +319,7 @@ class Glyph(object): return self.bitmap.height @staticmethod - def from_glyphslot(slot): + def from_glyphslot(char, slot): """Construct and return a Glyph object from a FreeType GlyphSlot.""" pixels = Glyph.unpack_mono_bitmap(slot.bitmap) width, height = slot.bitmap.width, slot.bitmap.rows @@ -200,7 +329,7 @@ class Glyph(object): # which means that the pixel values are multiples of 64. advance_width = slot.advance.x / 64 - return Glyph(pixels, width, height, top, advance_width) + return Glyph(char, pixels, width, height, top, advance_width) @staticmethod def unpack_mono_bitmap(bitmap): @@ -308,36 +437,51 @@ class Font(dict): # render a monochromatic bitmap representation. self._face.load_char(char, freetype.FT_LOAD_RENDER | freetype.FT_LOAD_TARGET_MONO) - return Glyph.from_glyphslot(self._face.glyph) + return Glyph.from_glyphslot(char, self._face.glyph) def _render_char(self, 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) + bitmap = Bitmap(char, 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, char_width] + bitmap.bitblt(glyph.bitmap, row) + self[char] = [bitmap, width, char_width] def stream_char(self, char, hmap, reverse): - outbuffer, _, _ = self[char] + bitmap, _, _ = self[char] + bitmap.display() if hmap: - gen = outbuffer.get_hbyte(reverse) + gen = bitmap.get_hbyte(reverse) else: - gen = outbuffer.get_vbyte(reverse) + gen = bitmap.get_vbyte(reverse) yield from gen + def build_lmap_arrays(self): + data = bytearray() + index = bytearray((0, 0)) + for char in self.charset: + bitmap, width, char_width = self[char] + bitmap.display() + data += byte(1 if bitmap.is_char_lhmap() else 0) + data += byte(width) + # data += byte_pair(width) + for b in bitmap.stream(): + data += b + index += byte_pair(len(data)) + return data, index + def build_arrays(self, hmap, reverse): data = bytearray() index = bytearray((0, 0)) for char in self.charset: width = self[char][1] - data += (width).to_bytes(2, byteorder='little') + data += byte_pair(width) data += bytearray(self.stream_char(char, hmap, reverse)) - index += (len(data)).to_bytes(2, byteorder='little') + index += byte_pair(len(data)) return data, index def build_binary_array(self, hmap, reverse, sig): @@ -350,34 +494,68 @@ class Font(dict): # PYTHON FILE WRITING -STR01 = """# Code generated by font-to-py.py. +HEADER = """# Code generated by font-to-py.py. +# Font: %(font)s +version = '0.2' + +def from_bytes(data): + return int.from_bytes(data, 'little') +""" + +HEADER_CHARSET = """# Code generated by font-to-py.py. # Font: %(font)s version = '0.2' CHARSET = %(charset)s + +def from_bytes(data): + return int.from_bytes(data, 'little') """ -STR02 = """_mvfont = memoryview(_font) +CHAR_BOUNDS = """\ +def _char_bounds(ch): + index = ord(ch) - %(minchar)d + offset = 2 * index + start = from_bytes(_index[offset:offset+2]) + next_offset = 2 * (index + 1) + end = from_bytes(_index[next_offset:next_offset+2]) + return start, end +""" +CHAR_BOUNDS_CHARSET = """\ def _char_bounds(ch): index = CHARSET[ch] offset = 2 * index - start = int.from_bytes(_index[offset:offset+2], 'little') + start = from_bytes(_index[offset:offset+2]) next_offset = 2 * (index + 1) - end = int.from_bytes(_index[next_offset:next_offset+2], 'little') + end = from_bytes(_index[next_offset:next_offset+2]) return start, end +""" + +GET_CHAR = """ +_mvfont = memoryview(_font) def get_char(ch): start, end = _char_bounds(ch) - width = int.from_bytes(_mvfont[start:start + 2], 'little') + width = from_bytes(_mvfont[start:start + 2]) return _mvfont[start + 2:end], %(height)s, width """ +GET_CHAR_LMAP = """ +_mvfont = memoryview(_font) + +def get_char(ch): + start, end = _char_bounds(ch) + is_lhmap = _mvfont[start] + width = _mvfont[start+1] + return is_lhmap, _mvfont[start + 2:end], %(height)s, width +""" + def write_func(stream, name, arg): stream.write('def {}():\n return {}\n\n'.format(name, arg)) -def write_font(op_path, font_path, height, monospaced, hmap, reverse, charset): +def write_font(op_path, font_path, height, monospaced, hmap, lmap, reverse, charset): try: fnt = Font(font_path, height, charset, monospaced) except freetype.ft_errors.FT_Exception: @@ -385,32 +563,48 @@ def write_font(op_path, font_path, height, monospaced, hmap, reverse, charset): return False try: with open(op_path, 'w') as stream: - write_data(stream, fnt, font_path, monospaced, hmap, reverse, charset) + write_data(stream, fnt, font_path, monospaced, hmap, lmap, 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, charset): +def write_data(stream, fnt, font_path, monospaced, hmap, lmap, reverse, charset): height = fnt.height # Actual height, not target height - stream.write(STR01 % {'font': os.path.split(font_path)[1], - 'charset': {ch: i for i, ch in enumerate(charset)} - }) + sequential_charset = not bool(len([x for x in range(len(charset) - 1) + if ord(charset[x]) + 1 != ord(charset[x+1])])) + header_data = {'font': os.path.split(font_path)[1], + 'charset': {ch: i for i, ch in enumerate(charset)}} + if sequential_charset: + stream.write(HEADER % header_data) + else: + stream.write(HEADER_CHARSET % header_data) stream.write('\n') write_func(stream, 'height', height) write_func(stream, 'max_width', fnt.max_width) write_func(stream, 'hmap', hmap) + write_func(stream, 'lmap', lmap) write_func(stream, 'reverse', reverse) write_func(stream, 'monospaced', monospaced) - data, index = fnt.build_arrays(hmap, reverse) + if lmap: + data, index = fnt.build_lmap_arrays() + else: + data, index = fnt.build_arrays(hmap, reverse) bw_font = ByteWriter(stream, '_font') bw_font.odata(data) bw_font.eot() bw_index = ByteWriter(stream, '_index') bw_index.odata(index) bw_index.eot() - stream.write(STR02 % {'height': height}) + if sequential_charset: + stream.write(CHAR_BOUNDS % {'minchar': ord(charset[0])}) + else: + stream.write(CHAR_BOUNDS_CHARSET) + if lmap: + stream.write(GET_CHAR_LMAP % {'height': height}) + else: + stream.write(GET_CHAR % {'height': height}) # BINARY OUTPUT # hmap reverse magic bytes @@ -470,6 +664,8 @@ if __name__ == "__main__": parser.add_argument('-x', '--xmap', action='store_true', help='Horizontal (x) mapping') + parser.add_argument('-L', '--lmap', action='store_true', + help='Line mapping') parser.add_argument('-r', '--reverse', action='store_true', help='Bit reversal') parser.add_argument('-f', '--fixed', action='store_true', @@ -519,7 +715,7 @@ if __name__ == "__main__": print('Writing Python font file.') if not write_font(args.outfile, args.infile, args.height, args.fixed, - args.xmap, args.reverse, charset): + args.xmap, args.lmap, args.reverse, charset): sys.exit(1) print(args.outfile, 'written successfully.') diff --git a/writer.py b/writer.py index c1219ba..500db67 100644 --- a/writer.py +++ b/writer.py @@ -28,64 +28,34 @@ # same Display object. +def from_bytes(data, signed=False): + return int.from_bytes(data, 'little', signed) + + class Writer(object): # these attributes and set_position are common to all Writer instances x_pos = 0 y_pos = 0 - device = None - screen_height = 0 - screen_width = 0 - draw_pixel = None @classmethod def set_position(cls, x, y): cls.x_pos = x cls.y_pos = y - def __init__(self, device, font, rotation=None): + def __init__(self, display, font): super().__init__() - self.device = device + self.set_display(display) self.set_font(font) - self.set_rotation(rotation) + + def set_display(self, display): + self.display = display def set_font(self, font): self.font = font - self._draw_char = self._draw_vmap_char - if font.hmap(): - self._draw_char = self._draw_hmap_char - - @classmethod - def set_rotation(cls, rotation=None): - rotation = 0 if not rotation else rotation % 360 - if not rotation: - cls.draw_pixel = cls._draw_pixel - elif rotation == 90: - cls.draw_pixel = cls._draw_pixel_90 - elif rotation == 180: - cls.draw_pixel = cls._draw_pixel_180 - elif rotation == 270: - cls.draw_pixel = cls._draw_pixel_270 + if font.lmap(): + self.draw_char = self._draw_lmap_char else: - raise ValueError('rotation must be falsy or one of 90, 180 or 270') - - if not rotation or rotation == 180: - cls.screen_width = cls.device.width - cls.screen_height = cls.device.height - else: - cls.screen_width = cls.device.height - cls.screen_height = cls.device.width - - def _draw_pixel(self, x, y, color): - self.device.pixel(x, y, color) - - def _draw_pixel_90(self, x, y, color): - self.device.pixel(self.device.width - y, x, color) - - def _draw_pixel_180(self, x, y, color): - self.device.pixel(self.device.width - x, self.device.height - y, color) - - def _draw_pixel_270(self, x, y, color): - self.device.pixel(y, self.device.height - x, color) + self.draw_char = self._draw_char def _newline(self): Writer.x_pos = 0 @@ -93,64 +63,114 @@ class Writer(object): def draw_text(self, string): for char in string: - self._draw_char(char) + self.draw_char(char) - def _draw_hmap_char(self, char): + def _draw_char(self, char): if char == '\n': self._newline() return glyph, char_height, char_width = self.font.get_char(char) - if Writer.x_pos + char_width > self.screen_width: + if Writer.x_pos + char_width > self.display.screen_width: self._newline() + if self.font.hmap(): + self._draw_hmap_char(glyph, char_height, char_width) + else: + self._draw_vmap_char(glyph, char_height, char_width) + + Writer.x_pos += char_width + + def _draw_hmap_char(self, glyph, char_height, char_width): div, mod = divmod(char_width, 8) bytes_per_row = div + 1 if mod else div for glyph_row_i in range(char_height): glyph_row_start = glyph_row_i * bytes_per_row - glyph_row = int.from_bytes( - glyph[glyph_row_start:glyph_row_start + bytes_per_row], - 'little' - ) + glyph_row = from_bytes(glyph[glyph_row_start:glyph_row_start + bytes_per_row]) if not glyph_row: continue x = Writer.x_pos y = Writer.y_pos + glyph_row_i for glyph_col_i in range(char_width): if glyph_row & (1 << glyph_col_i): - self.draw_pixel(x, y, 1) + self.display.pixel(x, y) x += 1 - Writer.x_pos += char_width - - def _draw_vmap_char(self, char): - if char == '\n': - self._newline() - return - - glyph, char_height, char_width = self.font.get_char(char) - - if Writer.x_pos + char_width > self.screen_width: - self._newline() - + def _draw_vmap_char(self, glyph, char_height, char_width): div, mod = divmod(char_height, 8) bytes_per_col = div + 1 if mod else div for glyph_col_i in range(char_width): glyph_col_start = glyph_col_i * bytes_per_col - glyph_col = int.from_bytes( - glyph[glyph_col_start:glyph_col_start + bytes_per_col], - 'little' - ) + glyph_col = from_bytes(glyph[glyph_col_start:glyph_col_start + bytes_per_col]) if not glyph_col: continue x = Writer.x_pos + glyph_col_i y = Writer.y_pos for glyph_row_i in range(char_height): if glyph_col & (1 << glyph_row_i): - self.draw_pixel(x, y, 1) + self.display.pixel(x, y) y += 1 + def _draw_lmap_char(self, char): + if char == '\n': + self._newline() + return + + is_lhmap, data, char_height, char_width = self.font.get_char(char) + + if Writer.x_pos + char_width > self.display.screen_width: + self._newline() + + if is_lhmap: + self._draw_lhmap_char(data) + else: + self._draw_lvmap_char(data) + Writer.x_pos += char_width + + def _draw_lhmap_char(self, data): + prev_lines = [] + y = 0 + data_i = 0 + while data_i < len(data): + num_lines = data[data_i] + if num_lines: + prev_lines = [] + y = Writer.y_pos + data[data_i + 1] + for i in range(num_lines): + lstart = data_i + 2 + (i * 2) + x = Writer.x_pos + data[lstart] + length = data[lstart + 1] + prev_lines.append((x, length)) + self.display.hline(x, y, length) + data_i = lstart + 2 + else: + y += 1 + for line in prev_lines: + self.display.hline(line[0], y, line[1]) + data_i += 1 + + def _draw_lvmap_char(self, data): + prev_lines = [] + x = 0 + data_i = 0 + while data_i < len(data): + num_lines = data[data_i] + if num_lines: + prev_lines = [] + x = Writer.x_pos + data[data_i + 1] + for i in range(num_lines): + lstart = data_i + 2 + (i * 2) + y = Writer.y_pos + data[lstart] + length = data[lstart + 1] + prev_lines.append((y, length)) + self.display.vline(x, y, length) + data_i = lstart + 2 + else: + x += 1 + for line in prev_lines: + self.display.vline(x, line[0], line[1]) + data_i += 1 From 12811b854281e815478fec3f0d75a68d1ffd26e2 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 11:22:02 -0400 Subject: [PATCH 04/16] do not display debug output while running font_to_py --- font_to_py.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/font_to_py.py b/font_to_py.py index 4c05f07..8c2d655 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -453,7 +453,6 @@ class Font(dict): def stream_char(self, char, hmap, reverse): bitmap, _, _ = self[char] - bitmap.display() if hmap: gen = bitmap.get_hbyte(reverse) else: @@ -465,10 +464,8 @@ class Font(dict): index = bytearray((0, 0)) for char in self.charset: bitmap, width, char_width = self[char] - bitmap.display() data += byte(1 if bitmap.is_char_lhmap() else 0) data += byte(width) - # data += byte_pair(width) for b in bitmap.stream(): data += b index += byte_pair(len(data)) From 247098465b151ab3a9e843b31a2b827edfeef15c Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 11:24:09 -0400 Subject: [PATCH 05/16] rename from_bytes to _from_bytes in generated font files --- font_to_py.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/font_to_py.py b/font_to_py.py index 8c2d655..739abb0 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -494,37 +494,36 @@ class Font(dict): HEADER = """# Code generated by font-to-py.py. # Font: %(font)s version = '0.2' - -def from_bytes(data): - return int.from_bytes(data, 'little') """ HEADER_CHARSET = """# Code generated by font-to-py.py. # Font: %(font)s version = '0.2' CHARSET = %(charset)s +""" -def from_bytes(data): +FROM_BYTES = """\ +def _from_bytes(data): return int.from_bytes(data, 'little') """ -CHAR_BOUNDS = """\ +CHAR_BOUNDS = """ def _char_bounds(ch): index = ord(ch) - %(minchar)d offset = 2 * index - start = from_bytes(_index[offset:offset+2]) + start = _from_bytes(_index[offset:offset+2]) next_offset = 2 * (index + 1) - end = from_bytes(_index[next_offset:next_offset+2]) + end = _from_bytes(_index[next_offset:next_offset+2]) return start, end """ -CHAR_BOUNDS_CHARSET = """\ +CHAR_BOUNDS_CHARSET = """ def _char_bounds(ch): index = CHARSET[ch] offset = 2 * index - start = from_bytes(_index[offset:offset+2]) + start = _from_bytes(_index[offset:offset+2]) next_offset = 2 * (index + 1) - end = from_bytes(_index[next_offset:next_offset+2]) + end = _from_bytes(_index[next_offset:next_offset+2]) return start, end """ @@ -533,7 +532,7 @@ _mvfont = memoryview(_font) def get_char(ch): start, end = _char_bounds(ch) - width = from_bytes(_mvfont[start:start + 2]) + width = _from_bytes(_mvfont[start:start + 2]) return _mvfont[start + 2:end], %(height)s, width """ @@ -594,6 +593,7 @@ def write_data(stream, fnt, font_path, monospaced, hmap, lmap, reverse, charset) bw_index = ByteWriter(stream, '_index') bw_index.odata(index) bw_index.eot() + stream.write(FROM_BYTES) if sequential_charset: stream.write(CHAR_BOUNDS % {'minchar': ord(charset[0])}) else: From 90998dd94e3651bfce15f77d410b78c71b096e4f Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 14:46:59 -0400 Subject: [PATCH 06/16] allow to specify color to writer --- writer.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/writer.py b/writer.py index 500db67..bd0cffc 100644 --- a/writer.py +++ b/writer.py @@ -42,10 +42,11 @@ class Writer(object): cls.x_pos = x cls.y_pos = y - def __init__(self, display, font): + def __init__(self, display, font, color=1): super().__init__() self.set_display(display) self.set_font(font) + self.set_color(color) def set_display(self, display): self.display = display @@ -57,15 +58,19 @@ class Writer(object): else: self.draw_char = self._draw_char + def set_color(self, color): + self.color = color + def _newline(self): Writer.x_pos = 0 Writer.y_pos += self.font.height() - def draw_text(self, string): + def draw_text(self, string, color=None): + color = color if color is not None else self.color for char in string: - self.draw_char(char) + self.draw_char(char, color) - def _draw_char(self, char): + def _draw_char(self, char, color): if char == '\n': self._newline() return @@ -76,13 +81,13 @@ class Writer(object): self._newline() if self.font.hmap(): - self._draw_hmap_char(glyph, char_height, char_width) + self._draw_hmap_char(glyph, char_height, char_width, color) else: - self._draw_vmap_char(glyph, char_height, char_width) + self._draw_vmap_char(glyph, char_height, char_width, color) Writer.x_pos += char_width - def _draw_hmap_char(self, glyph, char_height, char_width): + def _draw_hmap_char(self, glyph, char_height, char_width, color): div, mod = divmod(char_width, 8) bytes_per_row = div + 1 if mod else div @@ -95,10 +100,10 @@ class Writer(object): y = Writer.y_pos + glyph_row_i for glyph_col_i in range(char_width): if glyph_row & (1 << glyph_col_i): - self.display.pixel(x, y) + self.display.pixel(x, y, color) x += 1 - def _draw_vmap_char(self, glyph, char_height, char_width): + def _draw_vmap_char(self, glyph, char_height, char_width, color): div, mod = divmod(char_height, 8) bytes_per_col = div + 1 if mod else div @@ -111,10 +116,10 @@ class Writer(object): y = Writer.y_pos for glyph_row_i in range(char_height): if glyph_col & (1 << glyph_row_i): - self.display.pixel(x, y) + self.display.pixel(x, y, color) y += 1 - def _draw_lmap_char(self, char): + def _draw_lmap_char(self, char, color): if char == '\n': self._newline() return @@ -125,13 +130,13 @@ class Writer(object): self._newline() if is_lhmap: - self._draw_lhmap_char(data) + self._draw_lhmap_char(data, color) else: - self._draw_lvmap_char(data) + self._draw_lvmap_char(data, color) Writer.x_pos += char_width - def _draw_lhmap_char(self, data): + def _draw_lhmap_char(self, data, color): prev_lines = [] y = 0 data_i = 0 @@ -145,7 +150,7 @@ class Writer(object): x = Writer.x_pos + data[lstart] length = data[lstart + 1] prev_lines.append((x, length)) - self.display.hline(x, y, length) + self.display.hline(x, y, length, color) data_i = lstart + 2 else: y += 1 @@ -153,7 +158,7 @@ class Writer(object): self.display.hline(line[0], y, line[1]) data_i += 1 - def _draw_lvmap_char(self, data): + def _draw_lvmap_char(self, data, color): prev_lines = [] x = 0 data_i = 0 @@ -167,7 +172,7 @@ class Writer(object): y = Writer.y_pos + data[lstart] length = data[lstart + 1] prev_lines.append((y, length)) - self.display.vline(x, y, length) + self.display.vline(x, y, length, color) data_i = lstart + 2 else: x += 1 From e5bdcbe0985a5bec465e9bff539f2f20258a0e50 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 14:48:09 -0400 Subject: [PATCH 07/16] add explanation of line mapping format to DRIVERS doc --- DRIVERS.md | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/DRIVERS.md b/DRIVERS.md index 3a99cc4..78fa947 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -148,6 +148,9 @@ def max_width(): def hmap(): return False +def lmap(): + return False + def reverse(): return False @@ -175,7 +178,7 @@ def get_char(ch): # validate ch, if out of range use '?' # get offsets into _font and retrieve char width # Return: memoryview of bitmap, height and width - return mvfont[offset + 2, next_offset], height, width + return _mvfont[offset + 2, next_offset], height, width ``` ``height`` and ``width`` are specified in bits (pixels). @@ -191,6 +194,9 @@ character check. and contains all the bytes required to render the character including trailing space. +For line-mapped fonts, `get_char()` returns a 4-tuple: +(is_horizontal_mapping, memoryview_of_char_line_data, height, width) + ## Binary font files These are unlikely to find application beyond the e-paper driver, but for @@ -221,6 +227,8 @@ values represent locations to the right of the origin and increasing y values represent downward positions. Mapping defines the relationship between this abstract two dimensional array of bits and the physical linear sequence of bytes. +### Bit Mapping + Vertical mapping means that the LSB of first byte is pixel (0,0), MSB of first byte is (0, 7). The second byte (assuming the height is greater than 8 pixels) is (0, 8) to (0, 15). Once the column is complete the next byte represents @@ -233,6 +241,55 @@ than 8. Bit reversal provides for the case where the bit order of each byte is reversed i.e. a byte comprising bits [b7b6b5b4b3b2b1b0] becomes [b0b1b2b3b4b5b6b7]. +### Line Mapping + +There is also an alternative representation of storing characters, that can be +generated using `font_to_py.py`'s `-L` flag (for **L**ine mapping). Instead of +storing one high/low bit per pixel in the character space, it stores the lines +that make up the character. Lines within a character will either be horizontally +or vertically mapped, depending upon which mapping generates fewer draw calls. + +This directional flag is stored in the first byte of a character, with `1` +meaning horizontal and `0` meaning vertical. The second byte stores the width +of the character in pixels, with all subsequent bytes storing the line data. + +#### Horizontal Line Mapping + +For horizontally mapped line data, the format returned by `get_char` is as follows: +For each row in the character with pixel data, the first byte specifies how many +lines are in the row, and the second byte specifies the `y` coordinate of each of +the lines (with (0,0) being the top-left). Each line in the row then follows as +a pair of bytes, where the first byte is the starting `x` coordinate and the second +byte is the length of the line (in pixels). Once `num_lines * 2` bytes have been +exhausted, the next row starts. + +Subsequent rows usually follow the same format, however, there is one exception: +when the next row has the same data as the previous row. (Except of course for +the `y` coordinate, which should be incremented by `1`.) This is specified by a +single `0` byte. There can be multiple `0` bytes one after another, and that +means to use the most recent line data available, incrementing `y` by `1` for +each `0` byte encountered. + +A sample drawing implementation can be found [here](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer.py#L139-L159). + +#### Vertical Line Mapping + +For vertically mapped line data, the format returned by `get_char` is similar: +For each column in the character with pixel data, the first byte specifies how +many lines are in the column, and the second byte specifies the `x` coordinate +of each of the lines (again with (0,0) being the top-left). Each line in the +column then follows as a pair of bytes, where the first byte is the starting +`y` coordinate and the second byte is the length of the line (in pixels). Once +`num_lines * 2` bytes have been exhausted, the next column starts. + +Subsequent columns usually follow the same format, however, again there is one +exception: when the next column has the same data as the previous column. +(Except of course for the `x` coordinate, which should be incremented by `1`.) +Likewise this is specified by a single `0` byte, and there can be multiple `0` +bytes one after another, incrementing `x` by `1` for each `0` byte encountered. + +A sample drawing implementation can be found [here](https://github.com/peterhinch/micropython-font-to-py/blob/master/writer.py#L161-L181). + # Specification and Project Notes The design aims primarily to minimise RAM usage. Minimising the size of the From 3cf50e84c039bf0340123da87bc253a14cc95263 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 14:51:42 -0400 Subject: [PATCH 08/16] add arg parser to font_test for testing bitmapped fonts --- font_test.py | 57 +++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 16 deletions(-) diff --git a/font_test.py b/font_test.py index ef96d0f..96aa872 100644 --- a/font_test.py +++ b/font_test.py @@ -97,15 +97,17 @@ def display(data, hmap, height, width, reverse): # Basic test of Font class functionality # Usage font_test.font_test() def font_test(): - fnt = Font('FreeSans.ttf', 20) - for char in 'WM_eg!.,': + charset = 'WM_eg!.,' + fnt = Font('FreeSans.ttf', 20, charset, False) + for char in charset: fnt[char][0].display() print(fnt.width) + # Font character streaming # Usage font_test.test_stream('abc', 20, False, False, False) def test_stream(string, height, monospaced, hmap, reverse): - fnt = Font("FreeSans.ttf", height, monospaced) + fnt = Font("FreeSans.ttf", height, string, monospaced) height = fnt.height for char in string: width = fnt[char][1] @@ -121,7 +123,7 @@ def chr_addr(index, ordch): # Font.build_arrays # Usage font_test.test_arrays('abc', 20, False, False, False) def test_arrays(string, height, monospaced, hmap, reverse): - fnt = Font("FreeSans.ttf", height, monospaced) + fnt = Font("FreeSans.ttf", height, string, monospaced) height = fnt.height data, index = fnt.build_arrays(hmap, reverse) for char in string: @@ -140,23 +142,18 @@ def test_font(fontfile, string): del sys.modules[fontfile] # force reload myfont = import_module(fontfile) - height = myfont.height() - for row in range(height): - for char in string: - data, _, width = myfont.get_char(char) - if myfont.hmap(): - render_row_hmap(data, row, height, width, myfont.reverse()) - else: - render_row_vmap(data, row, height, width, myfont.reverse()) - print() + _display_bitmapped_string(myfont, string) # Create font file, render a string to REPL using it # usage font_test.test_file('FreeSans.ttf', 20, 'xyz') def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord('?'), - fixed=False, hmap=False, reverse=False): + fixed=False, hmap=False, lmap=False, reverse=False): + charset = [chr(x) for x in range(minchar, maxchar+1)] + if chr(defchar) not in charset: + charset += chr(defchar) if not write_font('myfont.py', fontfile, height, fixed, - hmap, reverse, minchar, maxchar, defchar): + hmap, lmap, reverse, charset): print('Failed to create font file.') return @@ -164,6 +161,11 @@ def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord( del sys.modules['myfont'] # force reload import myfont + _display_bitmapped_string(myfont, string) + os.unlink('myfont.py') + + +def _display_bitmapped_string(myfont, string): height = myfont.height() for row in range(height): for char in string: @@ -173,4 +175,27 @@ def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord( else: render_row_vmap(data, row, height, width, myfont.reverse()) print() - os.unlink('myfont.py') + + +if __name__ == '__main__': + import argparse + parser = argparse.ArgumentParser(description='Test font_to_py') + parser.add_argument('-f', '--font', dest='font', + help='Name of a Python font module generated by font_to_py') + parser.add_argument('-F', '--fontfile', dest='fontfile', + help='Path to a TTF font file to test') + parser.add_argument('-s', '--size', dest='size', default=20) + parser.add_argument('test_string', nargs='?', default='ABCD.efghij 123!') + args = parser.parse_args() + + if args.fontfile: + if not os.path.exists(args.fontfile): + exit('Sorry, could not find a font file at {}.'.format(args.fontfile)) + print('Running test_file with source font {}:'.format(args.fontfile)) + test_file(args.fontfile, int(args.size), args.test_string) + + elif args.font: + if '.py' in args.font: + args.font = args.font[:-3] + print('Running test_font with py font module {}:'.format(args.font)) + test_font(args.font, args.test_string) From 3e36da26b547ede103cdde0dee423fe4b691ddb9 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 15:07:28 -0400 Subject: [PATCH 09/16] add back original Bitmap.display() function --- font_to_py.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/font_to_py.py b/font_to_py.py index 739abb0..7b31414 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -118,6 +118,15 @@ class Bitmap(object): def display(self): """Print the bitmap's pixels.""" + for row in range(self.height): + for col in range(self.width): + char = '#' if self.pixels[row * self.width + col] else '.' + print(char, end='') + print() + print() + + def display_line_map(self): + """Print the bitmap's line map.""" lh_count = len(flatten(self.lh_data)) print('{} horizontal line mapping: {} hline draw calls. {} bytes'.format( self.char, From b555829618d5413cac0eb81e735add582de20558 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 16:24:44 -0400 Subject: [PATCH 10/16] add support for rendering line mapped fonts to font_test --- font_test.py | 113 ++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 99 insertions(+), 14 deletions(-) diff --git a/font_test.py b/font_test.py index 96aa872..ef0faba 100644 --- a/font_test.py +++ b/font_test.py @@ -47,6 +47,18 @@ def validate_vmap(data, height, width): # Routines to render to REPL +def render_bitmapped_string(myfont, string): + height = myfont.height() + for row in range(height): + for char in string: + data, _, width = myfont.get_char(char) + if myfont.hmap(): + render_row_hmap(data, row, height, width, myfont.reverse()) + else: + render_row_vmap(data, row, height, width, myfont.reverse()) + print() + + def render_row_hmap(data, row, height, width, reverse): validate_hmap(data, height, width) bytes_per_row = (width - 1)//8 + 1 @@ -73,6 +85,84 @@ def render_row_vmap(data, row, height, width, reverse): print(char, end='') +def render_linemapped_string(myfont, string): + height = myfont.height() + for row in range(height): + for char in string: + is_lhmap, data, _, width = myfont.get_char(char) + if is_lhmap: + render_row_lhmap(data, row, height, width) + else: + render_row_lvmap(data, row, height, width) + print() + + +def render_row_lhmap(data, row, height, width): + lines = [] + y = 0 + data_i = 0 + while data_i < len(data): + num_lines = data[data_i] + if num_lines: + y = data[data_i + 1] + while len(lines) <= y: + lines.append([]) + for i in range(num_lines): + lstart = data_i + 2 + (i * 2) + x = data[lstart] + length = data[lstart + 1] + lines[y].append((x, length)) + data_i = lstart + 2 + else: + lines.append(lines[-1]) + y += 1 + data_i += 1 + while len(lines) < height: + lines.append([]) + + x = 0 + for line in lines[row]: + while x < line[0]: + print('.', end='') + x += 1 + while x < line[0] + line[1]: + print('#', end='') + x += 1 + while x < width: + print('.', end='') + x += 1 + + +def render_row_lvmap(data, row, height, width): + lines = [] + x = 0 + data_i = 0 + while data_i < len(data): + num_lines = data[data_i] + if num_lines: + lines.append([]) + x = data[data_i + 1] + for i in range(num_lines): + lstart = data_i + 2 + (i * 2) + y = data[lstart] + length = data[lstart + 1] + lines[x].append((y, length)) + data_i = lstart + 2 + else: + lines.append(lines[-1]) + x += 1 + data_i += 1 + while len(lines) < width: + lines.append([]) + + for x in range(width): + char = '.' + for line in lines[x]: + if line[0] <= row < line[0] + line[1]: + char = '#' + print(char, end='') + + def display(data, hmap, height, width, reverse): bpr = (width - 1)//8 + 1 bpc = (height - 1)//8 + 1 @@ -142,7 +232,10 @@ def test_font(fontfile, string): del sys.modules[fontfile] # force reload myfont = import_module(fontfile) - _display_bitmapped_string(myfont, string) + if myfont.lmap(): + render_linemapped_string(myfont, string) + else: + render_bitmapped_string(myfont, string) # Create font file, render a string to REPL using it @@ -161,22 +254,14 @@ def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord( del sys.modules['myfont'] # force reload import myfont - _display_bitmapped_string(myfont, string) + if myfont.lmap(): + render_linemapped_string(myfont, string) + else: + render_bitmapped_string(myfont, string) + os.unlink('myfont.py') -def _display_bitmapped_string(myfont, string): - height = myfont.height() - for row in range(height): - for char in string: - data, _, width = myfont.get_char(char) - if myfont.hmap(): - render_row_hmap(data, row, height, width, myfont.reverse()) - else: - render_row_vmap(data, row, height, width, myfont.reverse()) - print() - - if __name__ == '__main__': import argparse parser = argparse.ArgumentParser(description='Test font_to_py') From 8255391465a4948551e39cf0715e00b582f05864 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 16:27:29 -0400 Subject: [PATCH 11/16] add missing color parameter to writer display.line calls --- writer.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/writer.py b/writer.py index bd0cffc..8d548b9 100644 --- a/writer.py +++ b/writer.py @@ -155,7 +155,7 @@ class Writer(object): else: y += 1 for line in prev_lines: - self.display.hline(line[0], y, line[1]) + self.display.hline(line[0], y, line[1], color) data_i += 1 def _draw_lvmap_char(self, data, color): @@ -177,5 +177,5 @@ class Writer(object): else: x += 1 for line in prev_lines: - self.display.vline(x, line[0], line[1]) + self.display.vline(x, line[0], line[1], color) data_i += 1 From d0f9da0fa5bcf18069d6986b3d715e19e67dad7c Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Thu, 29 Jun 2017 17:30:07 -0400 Subject: [PATCH 12/16] add color kwarg to display draw_text method --- display.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/display.py b/display.py index bce157d..75e08cd 100644 --- a/display.py +++ b/display.py @@ -19,13 +19,13 @@ class Display(ssd1306.SSD1306_I2C): def set_position(self, x, y): Writer.set_position(x, y) - def draw_text(self, text, x=None, y=None, font=None): + def draw_text(self, text, *, x=None, y=None, font=None, color=None): if x is not None and y is not None: Writer.set_position(x, y) if font: - font.draw_text(text) + font.draw_text(text, color) else: - self.default_font.draw_text(font) + self.default_font.draw_text(text, color) def clear(self): self.fill(0) From b6259d81fba2244e41e43613567e90ebebf67f9d Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Tue, 4 Jul 2017 18:37:52 -0400 Subject: [PATCH 13/16] add some comments to Writer methods --- writer.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/writer.py b/writer.py index 8d548b9..41e3d73 100644 --- a/writer.py +++ b/writer.py @@ -53,8 +53,16 @@ class Writer(object): def set_font(self, font): self.font = font + + # set self.draw_char depending on the mapping of the font if font.lmap(): self.draw_char = self._draw_lmap_char + elif font.hmap() and not font.reverse(): + # only reverse horizontal bit mapping is supported by this Writer + raise NotImplementedError + elif not font.hmap() and font.reverse(): + # reverse vertical bit mapping is not supported by this Writer + raise NotImplementedError else: self.draw_char = self._draw_char @@ -71,6 +79,9 @@ class Writer(object): self.draw_char(char, color) def _draw_char(self, char, color): + """ + Draw a bit mapped character + """ if char == '\n': self._newline() return @@ -88,6 +99,9 @@ class Writer(object): Writer.x_pos += char_width def _draw_hmap_char(self, glyph, char_height, char_width, color): + """ + Draw a horizontally & reverse bit mapped character + """ div, mod = divmod(char_width, 8) bytes_per_row = div + 1 if mod else div @@ -104,6 +118,9 @@ class Writer(object): x += 1 def _draw_vmap_char(self, glyph, char_height, char_width, color): + """ + Draw a vertically bit mapped character + """ div, mod = divmod(char_height, 8) bytes_per_col = div + 1 if mod else div @@ -120,6 +137,9 @@ class Writer(object): y += 1 def _draw_lmap_char(self, char, color): + """ + Draw a line mapped character + """ if char == '\n': self._newline() return @@ -137,6 +157,9 @@ class Writer(object): Writer.x_pos += char_width def _draw_lhmap_char(self, data, color): + """ + Draw a horizontally line mapped character + """ prev_lines = [] y = 0 data_i = 0 @@ -159,6 +182,9 @@ class Writer(object): data_i += 1 def _draw_lvmap_char(self, data, color): + """ + Draw a vertically line mapped character + """ prev_lines = [] x = 0 data_i = 0 From 9da8817f68b4b9de47fefd82b223cd68782e9cfc Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Tue, 4 Jul 2017 18:38:43 -0400 Subject: [PATCH 14/16] add some warnings to font_to_py for unsupported cli flag combinations --- font_to_py.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/font_to_py.py b/font_to_py.py index 7b31414..68c91ef 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -693,6 +693,15 @@ if __name__ == "__main__": args = parser.parse_args() + if args.lmap and args.xmap: + quit('Please select only one of line (L) mapping or horizontal (x) mapping') + + if args.lmap and args.reverse: + quit('Cannot use bit reversal with line mapping') + + if args.lmap and args.binary: + raise NotImplementedError + if not os.path.isfile(args.infile): quit("Font filename does not exist") From 45c247c4dff7a976fbc3dfbfb3ba9520f21d7176 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Fri, 7 Jul 2017 10:18:58 -0400 Subject: [PATCH 15/16] revert get_char to get_ch rename for BC --- DRIVERS.md | 12 ++++++------ FONT_TO_PY.md | 2 +- README.md | 2 +- font_test.py | 6 ++++-- font_to_py.py | 4 ++-- writer.py | 4 ++-- 6 files changed, 16 insertions(+), 14 deletions(-) diff --git a/DRIVERS.md b/DRIVERS.md index 78fa947..1ca8706 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -110,7 +110,7 @@ buffer/device. ### Specifying the font file -Each font file has a ``get_char()`` function accepting an ASCII character as its +Each font file has a ``get_ch()`` function accepting an ASCII character as its argument. It returns a memoryview instance providing access to a bytearray corresponding to the individual glyph. The layout of this data is determined by the command line arguments presented to the ``font_to_py.py`` utility. It is @@ -174,7 +174,7 @@ b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\ _mvfont = memoryview(_font) # Boilerplate code omitted -def get_char(ch): +def get_ch(ch): # validate ch, if out of range use '?' # get offsets into _font and retrieve char width # Return: memoryview of bitmap, height and width @@ -190,11 +190,11 @@ will fit the available space. If it will fit on the assumption that all chars are maximum width, it can be rendered rapidly without doing a character by character check. -``get_char()`` returns a memoryview of an individual glyph with its dimensions +``get_ch()`` returns a memoryview of an individual glyph with its dimensions and contains all the bytes required to render the character including trailing space. -For line-mapped fonts, `get_char()` returns a 4-tuple: +For line-mapped fonts, `get_ch()` returns a 4-tuple: (is_horizontal_mapping, memoryview_of_char_line_data, height, width) ## Binary font files @@ -255,7 +255,7 @@ of the character in pixels, with all subsequent bytes storing the line data. #### Horizontal Line Mapping -For horizontally mapped line data, the format returned by `get_char` is as follows: +For horizontally mapped line data, the format returned by `get_ch` is as follows: For each row in the character with pixel data, the first byte specifies how many lines are in the row, and the second byte specifies the `y` coordinate of each of the lines (with (0,0) being the top-left). Each line in the row then follows as @@ -274,7 +274,7 @@ A sample drawing implementation can be found [here](https://github.com/peterhinc #### Vertical Line Mapping -For vertically mapped line data, the format returned by `get_char` is similar: +For vertically mapped line data, the format returned by `get_ch` is similar: For each column in the character with pixel data, the first byte specifies how many lines are in the column, and the second byte specifies the `x` coordinate of each of the lines (again with (0,0) being the top-left). Each line in the diff --git a/FONT_TO_PY.md b/FONT_TO_PY.md index 8d7689a..6fdecd8 100644 --- a/FONT_TO_PY.md +++ b/FONT_TO_PY.md @@ -114,7 +114,7 @@ gc.collect() micropython.mem_info() def foo(): - addr, height, width = freeserif.get_char('a') + addr, height, width = freeserif.get_ch('a') foo() diff --git a/README.md b/README.md index 1ab17ec..8a6b420 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ provided to font-to-py: ``reverse`` Returns ``True`` if bit reversal was specified. Should return ``False`` ``monospaced`` Returns ``True`` if monospaced rendering was specified. -Glyphs are returned with the ``get_char`` method. Its argument is a character +Glyphs are returned with the ``get_ch`` method. Its argument is a character and it returns the following values: * A ``memoryview`` object containing the glyph bytes. diff --git a/font_test.py b/font_test.py index ef0faba..3158cec 100644 --- a/font_test.py +++ b/font_test.py @@ -51,7 +51,7 @@ def render_bitmapped_string(myfont, string): height = myfont.height() for row in range(height): for char in string: - data, _, width = myfont.get_char(char) + data, _, width = myfont.get_ch(char) if myfont.hmap(): render_row_hmap(data, row, height, width, myfont.reverse()) else: @@ -89,7 +89,7 @@ def render_linemapped_string(myfont, string): height = myfont.height() for row in range(height): for char in string: - is_lhmap, data, _, width = myfont.get_char(char) + is_lhmap, data, _, width = myfont.get_ch(char) if is_lhmap: render_row_lhmap(data, row, height, width) else: @@ -117,6 +117,8 @@ def render_row_lhmap(data, row, height, width): lines.append(lines[-1]) y += 1 data_i += 1 + if y == row: + break while len(lines) < height: lines.append([]) diff --git a/font_to_py.py b/font_to_py.py index 68c91ef..9b2449b 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -539,7 +539,7 @@ def _char_bounds(ch): GET_CHAR = """ _mvfont = memoryview(_font) -def get_char(ch): +def get_ch(ch): start, end = _char_bounds(ch) width = _from_bytes(_mvfont[start:start + 2]) return _mvfont[start + 2:end], %(height)s, width @@ -548,7 +548,7 @@ def get_char(ch): GET_CHAR_LMAP = """ _mvfont = memoryview(_font) -def get_char(ch): +def get_ch(ch): start, end = _char_bounds(ch) is_lhmap = _mvfont[start] width = _mvfont[start+1] diff --git a/writer.py b/writer.py index 41e3d73..73fbdf5 100644 --- a/writer.py +++ b/writer.py @@ -86,7 +86,7 @@ class Writer(object): self._newline() return - glyph, char_height, char_width = self.font.get_char(char) + glyph, char_height, char_width = self.font.get_ch(char) if Writer.x_pos + char_width > self.display.screen_width: self._newline() @@ -144,7 +144,7 @@ class Writer(object): self._newline() return - is_lhmap, data, char_height, char_width = self.font.get_char(char) + is_lhmap, data, char_height, char_width = self.font.get_ch(char) if Writer.x_pos + char_width > self.display.screen_width: self._newline() From e8fc2dcf54d512a2e38a612c9fd9106fc4207733 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Sat, 8 Jul 2017 21:38:41 -0400 Subject: [PATCH 16/16] update font version for line mapped format --- font_to_py.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/font_to_py.py b/font_to_py.py index 9b2449b..379efc3 100755 --- a/font_to_py.py +++ b/font_to_py.py @@ -502,12 +502,12 @@ class Font(dict): HEADER = """# Code generated by font-to-py.py. # Font: %(font)s -version = '0.2' +version = '%(version)s' """ HEADER_CHARSET = """# Code generated by font-to-py.py. # Font: %(font)s -version = '0.2' +version = '%(version)s' CHARSET = %(charset)s """ @@ -580,7 +580,8 @@ def write_data(stream, fnt, font_path, monospaced, hmap, lmap, reverse, charset) sequential_charset = not bool(len([x for x in range(len(charset) - 1) if ord(charset[x]) + 1 != ord(charset[x+1])])) header_data = {'font': os.path.split(font_path)[1], - 'charset': {ch: i for i, ch in enumerate(charset)}} + 'charset': {ch: i for i, ch in enumerate(charset)}, + 'version': '0.3' if lmap else '0.2'} if sequential_charset: stream.write(HEADER % header_data) else: