From 2ef0403ed4e94023db96b9a6c41a9a5748124715 Mon Sep 17 00:00:00 2001 From: Brian Cappello Date: Sun, 25 Jun 2017 19:50:04 -0400 Subject: [PATCH] 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