add font line mapping format + rendering code

pull/3/head
Brian Cappello 2017-06-25 19:50:04 -04:00
rodzic 85f90b0900
commit 2ef0403ed4
4 zmienionych plików z 461 dodań i 115 usunięć

96
display.py 100644
Wyświetl plik

@ -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)

34
display_test.py 100644
Wyświetl plik

@ -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

Wyświetl plik

@ -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.')

154
writer.py
Wyświetl plik

@ -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