kopia lustrzana https://github.com/peterhinch/micropython-font-to-py
Merge e8fc2dcf54
into de1e7cc65b
commit
39e167efd5
73
DRIVERS.md
73
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
|
||||
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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)
|
|
@ -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
|
|
@ -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()
|
||||
|
||||
|
||||
|
|
160
font_test.py
160
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_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)
|
||||
|
|
386
font_to_py.py
386
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,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
214
writer.py
|
@ -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
|
||||
|
|
Ładowanie…
Reference in New Issue