writer.py Simpler faster color code. Requires fw V1.17.

pull/45/head
Peter Hinch 2021-09-07 10:31:25 +01:00
rodzic 698444fa12
commit cee7862b8b
4 zmienionych plików z 373 dodań i 79 usunięć

Wyświetl plik

@ -76,10 +76,15 @@ The module has the following features:
Note that these changes have significantly increased code size. On the ESP8266
it is likely that `writer.py` will need to be frozen as bytecode. The original
very simple version still exists as `writer_minimal.py`.
very simple version still exists as `old_versions/writer_minimal.py`.
## 1.1 Release Notes
V0.5.0 Sep 2021
With the release of firmware V1.17, color display now requires this version.
This enabled the code to be simplified. For old firmware V0.4.3 is available as
`old_versions/writer_fw_compatible.py`.
V0.4.3 Aug 2021
Supports fast rendering of glyphs to color displays (PR7682). See
[Performance](./WRITER.md#223-performance).
@ -110,8 +115,6 @@ shows how to drive color displays using the `CWriter` class.
3. `writer_demo.py` Demo using a 128*64 SSD1306 OLED display. Import to see
usage information.
4. `writer_tests.py` Test/demo scripts. Import to see usage information.
5. `writer_minimal.py` A minimal version for highly resource constrained
devices.
Sample fonts:
1. `freesans20.py` Variable pitch font file.
@ -119,6 +122,12 @@ Sample fonts:
3. `font10.py` Smaller variable pitch fonts.
4. `font6.py`
Old versions (in `old_versions` directory):
1. `writer_minimal.py` A minimal version for highly resource constrained
devices.
2. `writer_fw_compatible.py` V0.4.3. Color display will run on firmware
versions < 1.17.
## 1.4 Fonts
Python font files should be created using `font-to-py.py` using horizontal
@ -145,7 +154,7 @@ ultra low power monochrome display.
The `Writer` class provides fast rendering to monochrome displays using bit
blitting. The `CWriter` class is a subclass of `Writer` to support color
displays which can now offer comparable performance (see below).
displays which now offers comparable performance (see below).
Multiple screens are supported. On any screen multiple `Writer` or `CWriter`
instances may be used, each using a different font. A class variable holds the
@ -264,6 +273,9 @@ This takes the following args:
4. `bgcolor=None` Background color. If `None` a monochrome display is assumed.
5. `verbose=True` If `True` the constructor emits console printout.
The constructor checks for suitable firmware and also for a compatible device
driver: an `OSError` is raised if these are absent.
### 2.2.2 Methods
All methods of the base class are supported. Additional method:
@ -281,24 +293,12 @@ rendered in foreground color on background color (or reversed if `invert` is
A firmware change [PR7682](https://github.com/micropython/micropython/pull/7682)
enables a substantial improvement to text rendering speed on color displays.
This is in daily builds and will be incorporated in V1.17. The initialisation
code checks for suitable firmware and also for a compatible device driver. If
these are absent the old slower method of rendering is used.
This was incorporated in firmware V1.17, and `writer.py` requires this or later
if using a color display.
The module has a `fast_mode` variable which is set `True` on import if the
firmware supports fast rendering. User code should treat this as read-only. The
value of this is meaningless for monochrome displays which always render fast.
If the `verbose` constructor arg is `True` a message will be printed on startup
indicating whether fast mode is in use. As above, this is meaningless for
monochrome displays. Possible reasons for it not being used:
* Firmware not recent enough.
* Display driver does not include a `palette` bound variable.
* `writer.py` not the current version.
The gain in speed depends on the font size, increasing for larger fonts.
Numbers may be found in `writer.py` code comments. Typical 10-20 pixel fonts
see gains on the order of 5-10 times.
The gain in speed resulting from this firmware change depends on the font size,
increasing for larger fonts. Numbers may be found in `writer.py` code comments.
Typical 10-20 pixel fonts see gains on the order of 5-10 times.
###### [Contents](./WRITER.md#contents)

Wyświetl plik

@ -0,0 +1,342 @@
# writer.py Implements the Writer class.
# Handles colour, word wrap and tab stops
# V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682).
# V0.4.0 Jan 2021 Improved handling of word wrap and line clip. Upside-down
# rendering no longer supported: delegate to device driver.
# V0.3.5 Sept 2020 Fast rendering option for color displays
# Released under the MIT License (MIT). See LICENSE.
# Copyright (c) 2019-2021 Peter Hinch
# A Writer supports rendering text to a Display instance in a given font.
# Multiple Writer instances may be created, each rendering a font to the
# same Display object.
# Timings were run on a pyboard D SF6W comparing slow and fast rendering
# and averaging over multiple characters. Proportional fonts were used.
# 20 pixel high font, timings were 5.44ms/467μs, gain 11.7 (freesans20).
# 10 pixel high font, timings were 1.76ms/396μs, gain 4.36 (arial10).
import framebuf
from uctypes import bytearray_at, addressof
from sys import implementation
import os
__version__ = (0, 4, 3)
def buildcheck(device):
if not hasattr(device, 'palette'):
return False
i0, i1, _ = implementation[1]
if i0 > 1 or i1 > 16:
return True
# After release of V1.17 require that build. Until then check for date.
# TODO simplify this once V1.17 is released.
try:
datestring = os.uname()[3]
date = datestring.split(' on')[1]
date = date.lstrip()[:10]
idate = tuple([int(x) for x in date.split('-')])
return idate >= (2021, 8, 25)
except AttributeError:
return False
fast_mode = False # False for mono displays although actually these render fast
class DisplayState():
def __init__(self):
self.text_row = 0
self.text_col = 0
def _get_id(device):
if not isinstance(device, framebuf.FrameBuffer):
raise ValueError('Device must be derived from FrameBuffer.')
return id(device)
# Basic Writer class for monochrome displays
class Writer():
state = {} # Holds a display state for each device
@staticmethod
def set_textpos(device, row=None, col=None):
devid = _get_id(device)
if devid not in Writer.state:
Writer.state[devid] = DisplayState()
s = Writer.state[devid] # Current state
if row is not None:
if row < 0 or row >= device.height:
raise ValueError('row is out of range')
s.text_row = row
if col is not None:
if col < 0 or col >= device.width:
raise ValueError('col is out of range')
s.text_col = col
return s.text_row, s.text_col
def __init__(self, device, font, verbose=True):
self.devid = _get_id(device)
self.device = device
if self.devid not in Writer.state:
Writer.state[self.devid] = DisplayState()
self.font = font
if font.height() >= device.height or font.max_width() >= device.width:
raise ValueError('Font too large for screen')
# Allow to work with reverse or normal font mapping
if font.hmap():
self.map = framebuf.MONO_HMSB if font.reverse() else framebuf.MONO_HLSB
else:
raise ValueError('Font must be horizontally mapped.')
if verbose:
fstr = 'Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}.'
print(fstr.format(font.reverse(), device.width, device.height))
print('Start row = {} col = {}'.format(self._getstate().text_row, self._getstate().text_col))
self.screenwidth = device.width # In pixels
self.screenheight = device.height
self.bgcolor = 0 # Monochrome background and foreground colors
self.fgcolor = 1
self.row_clip = False # Clip or scroll when screen fullt
self.col_clip = False # Clip or new line when row is full
self.wrap = True # Word wrap
self.cpos = 0
self.tab = 4
self.glyph = None # Current char
self.char_height = 0
self.char_width = 0
self.clip_width = 0
def _getstate(self):
return Writer.state[self.devid]
def _newline(self):
s = self._getstate()
height = self.font.height()
s.text_row += height
s.text_col = 0
margin = self.screenheight - (s.text_row + height)
y = self.screenheight + margin
if margin < 0:
if not self.row_clip:
self.device.scroll(0, margin)
self.device.fill_rect(0, y, self.screenwidth, abs(margin), self.bgcolor)
s.text_row += margin
def set_clip(self, row_clip=None, col_clip=None, wrap=None):
if row_clip is not None:
self.row_clip = row_clip
if col_clip is not None:
self.col_clip = col_clip
if wrap is not None:
self.wrap = wrap
return self.row_clip, self.col_clip, self.wrap
@property
def height(self): # Property for consistency with device
return self.font.height()
def printstring(self, string, invert=False):
# word wrapping. Assumes words separated by single space.
q = string.split('\n')
last = len(q) - 1
for n, s in enumerate(q):
if s:
self._printline(s, invert)
if n != last:
self._printchar('\n')
def _printline(self, string, invert):
rstr = None
if self.wrap and self.stringlen(string, True): # Length > self.screenwidth
pos = 0
lstr = string[:]
while self.stringlen(lstr, True): # Length > self.screenwidth
pos = lstr.rfind(' ')
lstr = lstr[:pos].rstrip()
if pos > 0:
rstr = string[pos + 1:]
string = lstr
for char in string:
self._printchar(char, invert)
if rstr is not None:
self._printchar('\n')
self._printline(rstr, invert) # Recurse
def stringlen(self, string, oh=False):
sc = self._getstate().text_col # Start column
wd = self.screenwidth
l = 0
for char in string[:-1]:
_, _, char_width = self.font.get_ch(char)
l += char_width
if oh and l + sc > wd:
return True # All done. Save time.
char = string[-1]
_, _, char_width = self.font.get_ch(char)
if oh and l + sc + char_width > wd:
l += self._truelen(char) # Last char might have blank cols on RHS
else:
l += char_width # Public method. Return same value as old code.
return l + sc > wd if oh else l
# Return the printable width of a glyph less any blank columns on RHS
def _truelen(self, char):
glyph, ht, wd = self.font.get_ch(char)
div, mod = divmod(wd, 8)
gbytes = div + 1 if mod else div # No. of bytes per row of glyph
mc = 0 # Max non-blank column
data = glyph[(wd - 1) // 8] # Last byte of row 0
for row in range(ht): # Glyph row
for col in range(wd -1, -1, -1): # Glyph column
gbyte, gbit = divmod(col, 8)
if gbit == 0: # Next glyph byte
data = glyph[row * gbytes + gbyte]
if col <= mc:
break
if data & (1 << (7 - gbit)): # Pixel is lit (1)
mc = col # Eventually gives rightmost lit pixel
break
if mc + 1 == wd:
break # All done: no trailing space
print('Truelen', char, wd, mc + 1) # TEST
return mc + 1
def _get_char(self, char, recurse):
if not recurse: # Handle tabs
if char == '\n':
self.cpos = 0
elif char == '\t':
nspaces = self.tab - (self.cpos % self.tab)
if nspaces == 0:
nspaces = self.tab
while nspaces:
nspaces -= 1
self._printchar(' ', recurse=True)
self.glyph = None # All done
return
self.glyph = None # Assume all done
if char == '\n':
self._newline()
return
glyph, char_height, char_width = self.font.get_ch(char)
s = self._getstate()
np = None # Allow restriction on printable columns
if s.text_row + char_height > self.screenheight:
if self.row_clip:
return
self._newline()
oh = s.text_col + char_width - self.screenwidth # Overhang (+ve)
if oh > 0:
if self.col_clip or self.wrap:
np = char_width - oh # No. of printable columns
if np <= 0:
return
else:
self._newline()
self.glyph = glyph
self.char_height = char_height
self.char_width = char_width
self.clip_width = char_width if np is None else np
# Method using blitting. Efficient rendering for monochrome displays.
# Tested on SSD1306. Invert is for black-on-white rendering.
def _printchar(self, char, invert=False, recurse=False):
s = self._getstate()
self._get_char(char, recurse)
if self.glyph is None:
return # All done
buf = bytearray(self.glyph)
if invert:
for i, v in enumerate(buf):
buf[i] = 0xFF & ~ v
fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map)
self.device.blit(fbc, s.text_col, s.text_row)
s.text_col += self.char_width
self.cpos += 1
def tabsize(self, value=None):
if value is not None:
self.tab = value
return self.tab
def setcolor(self, *_):
return self.fgcolor, self.bgcolor
# Writer for colour displays.
class CWriter(Writer):
def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True):
super().__init__(device, font, verbose)
global fast_mode
fast_mode = buildcheck(device)
if bgcolor is not None: # Assume monochrome.
self.bgcolor = bgcolor
if fgcolor is not None:
self.fgcolor = fgcolor
self.def_bgcolor = self.bgcolor
self.def_fgcolor = self.fgcolor
self._printchar = self._pchfast if fast_mode else self._pchslow
verbose and print('Render {} using fast mode'.format('is' if fast_mode else 'not'))
def _pchfast(self, char, invert=False, recurse=False):
s = self._getstate()
self._get_char(char, recurse)
if self.glyph is None:
return # All done
buf = bytearray_at(addressof(self.glyph), len(self.glyph))
fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map)
palette = self.device.palette
palette.bg(self.fgcolor if invert else self.bgcolor)
palette.fg(self.bgcolor if invert else self.fgcolor)
self.device.blit(fbc, s.text_col, s.text_row, -1, palette)
s.text_col += self.char_width
self.cpos += 1
def _pchslow(self, char, invert=False, recurse=False):
s = self._getstate()
self._get_char(char, recurse)
if self.glyph is None:
return # All done
char_height = self.char_height
char_width = self.char_width
clip_width = self.clip_width
div, mod = divmod(char_width, 8)
gbytes = div + 1 if mod else div # No. of bytes per row of glyph
device = self.device
fgcolor = self.bgcolor if invert else self.fgcolor
bgcolor = self.fgcolor if invert else self.bgcolor
drow = s.text_row # Destination row
wcol = s.text_col # Destination column of character start
for srow in range(char_height): # Source row
for scol in range(clip_width): # Source column
# Destination column: add writer column
dcol = wcol + scol
gbyte, gbit = divmod(scol, 8)
if gbit == 0: # Next glyph byte
data = self.glyph[srow * gbytes + gbyte]
pixel = fgcolor if data & (1 << (7 - gbit)) else bgcolor
device.pixel(dcol, drow, pixel)
drow += 1
if drow >= self.screenheight or drow < 0:
break
s.text_col += char_width
self.cpos += 1
def setcolor(self, fgcolor=None, bgcolor=None):
if fgcolor is None and bgcolor is None:
self.fgcolor = self.def_fgcolor
self.bgcolor = self.def_bgcolor
else:
if fgcolor is not None:
self.fgcolor = fgcolor
if bgcolor is not None:
self.bgcolor = bgcolor
return self.fgcolor, self.bgcolor

Wyświetl plik

@ -1,6 +1,7 @@
# writer.py Implements the Writer class.
# Handles colour, word wrap and tab stops
# V0.5.0 Sep 2021 Color now requires firmware >= 1.17.
# V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682).
# V0.4.0 Jan 2021 Improved handling of word wrap and line clip. Upside-down
# rendering no longer supported: delegate to device driver.
@ -24,27 +25,9 @@ from uctypes import bytearray_at, addressof
from sys import implementation
import os
__version__ = (0, 4, 3)
__version__ = (0, 5, 0)
def buildcheck(device):
if not hasattr(device, 'palette'):
return False
i0, i1, _ = implementation[1]
if i0 > 1 or i1 > 16:
return True
# After release of V1.17 require that build. Until then check for date.
# TODO simplify this once V1.17 is released.
try:
datestring = os.uname()[3]
date = datestring.split(' on')[1]
date = date.lstrip()[:10]
idate = tuple([int(x) for x in date.split('-')])
return idate >= (2021, 8, 25)
except AttributeError:
return False
fast_mode = False # False for mono displays although actually these render fast
fast_mode = True # Does nothing. Kept to avoid breaking code.
class DisplayState():
def __init__(self):
@ -202,7 +185,7 @@ class Writer():
break
if mc + 1 == wd:
break # All done: no trailing space
print('Truelen', char, wd, mc + 1) # TEST
# print('Truelen', char, wd, mc + 1) # TEST
return mc + 1
def _get_char(self, char, recurse):
@ -272,19 +255,20 @@ class CWriter(Writer):
def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True):
if not hasattr(device, 'palette'):
raise OSError('Incompatible device driver.')
if implementation[1] < (1, 17, 0):
raise OSError('Firmware must be >= 1.17.')
super().__init__(device, font, verbose)
global fast_mode
fast_mode = buildcheck(device)
if bgcolor is not None: # Assume monochrome.
self.bgcolor = bgcolor
if fgcolor is not None:
self.fgcolor = fgcolor
self.def_bgcolor = self.bgcolor
self.def_fgcolor = self.fgcolor
self._printchar = self._pchfast if fast_mode else self._pchslow
verbose and print('Render {} using fast mode'.format('is' if fast_mode else 'not'))
def _pchfast(self, char, invert=False, recurse=False):
def _printchar(self, char, invert=False, recurse=False):
s = self._getstate()
self._get_char(char, recurse)
if self.glyph is None:
@ -294,42 +278,10 @@ class CWriter(Writer):
palette = self.device.palette
palette.bg(self.fgcolor if invert else self.bgcolor)
palette.fg(self.bgcolor if invert else self.fgcolor)
self.device.blit(fbc, s.text_col, s.text_row, -1, palette)
s.text_col += self.char_width
self.cpos += 1
def _pchslow(self, char, invert=False, recurse=False):
s = self._getstate()
self._get_char(char, recurse)
if self.glyph is None:
return # All done
char_height = self.char_height
char_width = self.char_width
clip_width = self.clip_width
div, mod = divmod(char_width, 8)
gbytes = div + 1 if mod else div # No. of bytes per row of glyph
device = self.device
fgcolor = self.bgcolor if invert else self.fgcolor
bgcolor = self.fgcolor if invert else self.bgcolor
drow = s.text_row # Destination row
wcol = s.text_col # Destination column of character start
for srow in range(char_height): # Source row
for scol in range(clip_width): # Source column
# Destination column: add writer column
dcol = wcol + scol
gbyte, gbit = divmod(scol, 8)
if gbit == 0: # Next glyph byte
data = self.glyph[srow * gbytes + gbyte]
pixel = fgcolor if data & (1 << (7 - gbit)) else bgcolor
device.pixel(dcol, drow, pixel)
drow += 1
if drow >= self.screenheight or drow < 0:
break
s.text_col += char_width
self.cpos += 1
def setcolor(self, fgcolor=None, bgcolor=None):
if fgcolor is None and bgcolor is None:
self.fgcolor = self.def_fgcolor