Brian Cappello 2017-09-02 06:00:10 +00:00 zatwierdzone przez GitHub
commit 39e167efd5
8 zmienionych plików z 793 dodań i 182 usunięć

Wyświetl plik

@ -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
@ -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_ch(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_ch()` 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_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
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_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
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

Wyświetl plik

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

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, color=None):
if x is not None and y is not None:
Writer.set_position(x, y)
if font:
font.draw_text(text, color)
else:
self.default_font.draw_text(text, color)
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

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

Wyświetl plik

@ -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_ch(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,86 @@ 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_ch(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
if y == row:
break
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
@ -97,15 +189,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 +215,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 +234,21 @@ 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_ch(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 myfont.lmap():
render_linemapped_string(myfont, string)
else:
render_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,13 +256,33 @@ def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord(
del sys.modules['myfont'] # force reload
import myfont
height = myfont.height()
for row in range(height):
for char in string:
data, _, width = myfont.get_ch(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 myfont.lmap():
render_linemapped_string(myfont, string)
else:
render_bitmapped_string(myfont, string)
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)

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,16 +94,27 @@ 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."""
@ -109,6 +125,43 @@ class Bitmap(object):
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,
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(' ', '%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 +175,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 +328,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 +338,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):
@ -255,13 +393,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,66 +410,84 @@ 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.
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._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)
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]
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]
data += byte(1 if bitmap.is_char_lhmap() else 0)
data += byte(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):
@ -346,66 +500,118 @@ class Font(dict):
# PYTHON FILE WRITING
STR01 = """# Code generated by font-to-py.py.
# Font: {}
version = '0.2'
HEADER = """# Code generated by font-to-py.py.
# Font: %(font)s
version = '%(version)s'
"""
STR02 = """_mvfont = memoryview(_font)
HEADER_CHARSET = """# Code generated by font-to-py.py.
# Font: %(font)s
version = '%(version)s'
CHARSET = %(charset)s
"""
def _chr_addr(ordch):
offset = 2 * (ordch - {})
return int.from_bytes(_index[offset:offset + 2], 'little')
FROM_BYTES = """\
def _from_bytes(data):
return int.from_bytes(data, 'little')
"""
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 = _from_bytes(_index[offset:offset+2])
next_offset = 2 * (index + 1)
end = _from_bytes(_index[next_offset:next_offset+2])
return start, end
"""
GET_CHAR = """
_mvfont = memoryview(_font)
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
start, end = _char_bounds(ch)
width = _from_bytes(_mvfont[start:start + 2])
return _mvfont[start + 2:end], %(height)s, width
"""
GET_CHAR_LMAP = """
_mvfont = memoryview(_font)
def get_ch(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))
# 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, lmap, 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, 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, minchar, maxchar):
def write_data(stream, fnt, font_path, monospaced, hmap, lmap, reverse, charset):
height = fnt.height # Actual height, not target height
stream.write(STR01.format(os.path.split(font_path)[1]))
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)},
'version': '0.3' if lmap else '0.2'}
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)
write_func(stream, 'min_ch', minchar)
write_func(stream, 'max_ch', maxchar)
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.format(minchar, minchar, maxchar, minchar, height))
stream.write(FROM_BYTES)
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
@ -465,6 +671,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',
@ -474,22 +682,26 @@ 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 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")
@ -512,20 +724,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.lmap, args.reverse, charset):
sys.exit(1)
print(args.outfile, 'written successfully.')

214
writer.py
Wyświetl plik

@ -28,72 +28,180 @@
# same Display object.
def from_bytes(data, signed=False):
return int.from_bytes(data, 'little', signed)
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
@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, display, font, color=1):
super().__init__()
self.device = device
self.set_display(display)
self.set_font(font)
self.set_color(color)
def set_display(self, display):
self.display = display
def set_font(self, font):
self.font = font
if font.hmap():
raise OSError('Font must be vertically mapped')
self.screenwidth = device.width # In pixels
self.screenheight = device.height
# 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
def set_color(self, color):
self.color = 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, color=None):
color = color if color is not None else self.color
for char in string:
self._printchar(char)
self.draw_char(char, color)
def _printchar(self, char):
def _draw_char(self, char, color):
"""
Draw a bit mapped character
"""
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
self._newline()
if Writer.text_col + char_width > self.screenwidth:
if Writer.col_clip:
return
else:
self._newline()
glyph, char_height, char_width = self.font.get_ch(char)
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, color)
else:
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, color):
"""
Draw a horizontally & reverse bit mapped character
"""
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 = 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.display.pixel(x, y, color)
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)
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 = 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.display.pixel(x, y, color)
y += 1
def _draw_lmap_char(self, char, color):
"""
Draw a line mapped character
"""
if char == '\n':
self._newline()
return
is_lhmap, data, char_height, char_width = self.font.get_ch(char)
if Writer.x_pos + char_width > self.display.screen_width:
self._newline()
if is_lhmap:
self._draw_lhmap_char(data, color)
else:
self._draw_lvmap_char(data, color)
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
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, color)
data_i = lstart + 2
else:
y += 1
for line in prev_lines:
self.display.hline(line[0], y, line[1], color)
data_i += 1
def _draw_lvmap_char(self, data, color):
"""
Draw a vertically line mapped character
"""
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, color)
data_i = lstart + 2
else:
x += 1
for line in prev_lines:
self.display.vline(x, line[0], line[1], color)
data_i += 1