kopia lustrzana https://github.com/peterhinch/micropython-font-to-py
Docs updated
rodzic
bb4c0415d6
commit
1bcf06c531
|
@ -0,0 +1,216 @@
|
|||
# DRIVERS.md
|
||||
|
||||
This document comprises two sections, the first for users of an existing device
|
||||
driver and the second for writers of device drivers.
|
||||
|
||||
# User Documentation: the Writer class
|
||||
|
||||
This class facilitates rendering characters from Python font files to a device,
|
||||
assuming the device has a driver conforming to the specification below. Typical
|
||||
use is as follows:
|
||||
|
||||
```python
|
||||
from writer import Writer
|
||||
from device_driver import Display
|
||||
import freeserif
|
||||
import freesans20
|
||||
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')
|
||||
|
||||
display.show() # Display the result
|
||||
```
|
||||
|
||||
The file ``driver_test.py`` illustrates the use of font files with an SSD1306
|
||||
display and a complete example of an SSD1306 driver may be found
|
||||
[here](https://github.com/peterhinch/micropython-samples/tree/master/SSD1306).
|
||||
|
||||
## Class Methods
|
||||
|
||||
The ``Writer`` class exposes the following class methods:
|
||||
|
||||
1. ``set_textpos`` Args: ``row``, ``col``. 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
|
||||
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
|
||||
the physical display. If ``col_clip`` is ``False`` characters will wrap onto
|
||||
the next line. If ``row_clip`` is ``False`` the display will, where necessary,
|
||||
scroll up to ensure the line is rendered.
|
||||
3. ``mapping`` Arg: an integer. This defines the mapping of bytes in the
|
||||
buffer onto pixels. The module exposes three constants for use here: ``VERT``
|
||||
``HORIZ`` and ``WEIRD``, the latter being specific to the official SSD1306
|
||||
driver. ``VERT`` is for true vertically mapped displays. ``HORIZ``, for
|
||||
horizontally mapped devices, is currently unsupported. By default the mapping
|
||||
is for SSD1306 devices using the official driver.
|
||||
|
||||
As class methods these settings apply to all font objects. The insertion point
|
||||
of characters is maintained regardless of the font in use.
|
||||
|
||||
## Method
|
||||
|
||||
1. ``printstring`` Arg: a text string. Outputs a text string at the current
|
||||
insertion point. Newline characters are honoured.
|
||||
|
||||
# Device Driver Implementation
|
||||
|
||||
Display devices comprise two varieties, depending on whether the framebuffer is
|
||||
located on the controlling system or on the physical display device. In the
|
||||
former case the ``Writer`` class simplifies the design of the driver. It merely
|
||||
has to expose certin attributes and methods with ``Writer`` instances taking
|
||||
care of text rendering. It is strongly recommended that such device drivers use
|
||||
the oficial ``framebuf`` module, as per the official SSD1306 driver.
|
||||
|
||||
Where the buffer is located on the display device the means of controlling the
|
||||
text insertion point will be device dependent. The driver will need to
|
||||
implement the functionality of the ``Writer`` class itself.
|
||||
|
||||
## Drivers with local buffers
|
||||
|
||||
The writer of a device driver need not be concerned with the structure of a
|
||||
Python font file so long as the driver exposes certain attributes and methods
|
||||
required by the ``Writer`` class. These are as follows:
|
||||
|
||||
Attributes:
|
||||
|
||||
1. ``buffer`` The underlying ``bytearray`` instance holding the display
|
||||
buffer.
|
||||
2. ``height`` The screen height in pixels.
|
||||
3. ``width`` The screen width in pixels.
|
||||
|
||||
Methods:
|
||||
|
||||
1. ``show`` Display the current buffer contents.
|
||||
2. ``scroll`` Arguments ``x``, ``y`` amount to scroll horizontal and vertical.
|
||||
3. ``fill`` Argument ``col`` colour 1 == fill 0 == clear.
|
||||
|
||||
An example of such a driver, using the official ``framebuf`` module, is the
|
||||
SSD1306 driver (drivers/display/ssd1306.py in the source tree).
|
||||
|
||||
The driver documentation should specify the arguments for font_to_py.py to
|
||||
ensure users create font files with a layout corresponding to that of the
|
||||
buffer/device.
|
||||
|
||||
## Drivers for remote buffers
|
||||
|
||||
### Specifying the font file
|
||||
|
||||
Each font file has a ``get_ch()`` function accepting an ASCII character as its
|
||||
argument. It returns a memoryview instance providing access to a bytearray
|
||||
corresponding to the individual glyph. The layout of this data is determined by
|
||||
the command line arguments presented to the ``font_to_py.py`` utility. The
|
||||
driver documentation should specify these to ensure the layout is optimal to
|
||||
simplify and speed transfer to the device. Mapping may be horizontal or
|
||||
vertical, and the bit order of individual bytes may be defined. These are
|
||||
detailed below.
|
||||
|
||||
The Python source file provides a fast means of accessing the byte data
|
||||
corresponding to an individual character. It is the responsibility of the
|
||||
driver to copy that data to the physical device. The ``font_to_py.py``
|
||||
utility has command line arguments specifying the organisation of data returned
|
||||
by the font's ``get_ch()`` function.
|
||||
|
||||
The purpose of the command line arguments specified to the user is to
|
||||
ensure that the data layout is optimised for the device so that the copy is a
|
||||
simple bytewise copy or SPI/I2C transfer.
|
||||
|
||||
In the case of devices with their own frame buffer the Writer class will need
|
||||
to be re-written or adapted to match the hardware's method of tracking such
|
||||
things as the text insertion point. Consider maintaining the class's interface
|
||||
to simplify the writing of user code capable of being ported between displays.
|
||||
|
||||
## Font files
|
||||
|
||||
Assume the user has run the utility to produce a file ``myfont.py`` This then
|
||||
has the following outline definition (in practice the bytes objects are large):
|
||||
|
||||
```python
|
||||
# Code generated by font-to-py.py.
|
||||
# Font: FreeSerif.ttf
|
||||
version = '0.1'
|
||||
|
||||
def height():
|
||||
return 21
|
||||
|
||||
def max_width():
|
||||
return 22
|
||||
|
||||
def hmap():
|
||||
return False
|
||||
|
||||
def reverse():
|
||||
return False
|
||||
|
||||
def monospaced():
|
||||
return False
|
||||
|
||||
_font =\
|
||||
b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
|
||||
b'\x00\x00\x00\x00\x08\x00\xfe\xc7\x00\x7e\xc0\x00\x00\x00\x00\x00'\
|
||||
|
||||
_index =\
|
||||
b'\x00\x00\x14\x00\x2e\x00\x4b\x00\x71\x00\x97\x00\xd2\x00\x0a\x01'\
|
||||
b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\
|
||||
|
||||
# Boilerplate code omitted
|
||||
|
||||
def get_ch(ch):
|
||||
# validate ch, if out of range use '?'
|
||||
# get offsets into _font and retrieve char width
|
||||
# Return: address of start of bitmap, height and width
|
||||
return memoryview(_font[offset + 2, next_offset]), height, width
|
||||
```
|
||||
|
||||
``height`` and ``width`` are specified in bits (pixels).
|
||||
|
||||
In the case of monospaced fonts the ``max_width`` function returns the width of
|
||||
every character. For variable pitch fonts it returns the width of the widest
|
||||
character. Device drivers can use this to rapidly determine whether a string
|
||||
will fit the available space. If it will fit on the assumption that all chars
|
||||
are maximum width, it can be rendered rapidly without doing a character by
|
||||
character check.
|
||||
|
||||
``get_ch()`` returns a memoryview of an individual glyph with its dimensions
|
||||
and contains all the bytes required to render the character including trailing
|
||||
space.
|
||||
|
||||
## Mapping
|
||||
|
||||
A character occupies a space where (0, 0) represents the coordinates of the top
|
||||
left hand corner of the bitmap. It comprises a set of pixels where increasing x
|
||||
values represent locations to the right of the origin and increasing y values
|
||||
represent downward positions. Mapping is the process whereby this two
|
||||
dimensional array of bits is transformed into a linear sequence of bytes.
|
||||
|
||||
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
|
||||
(1, 0) to (1, 7).
|
||||
|
||||
Horizontal mapping means that the MSB of byte 0 is pixel (0,0) with LSB at
|
||||
(7,0), with the second byte covering (8, 0) to (15, 0) if the width is greater
|
||||
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].
|
||||
|
||||
# Specification and Project Notes
|
||||
|
||||
The design aims primarily to minimise RAM usage. Minimising the size of the
|
||||
bytecode is a secondary aim. Indexed addressing is used to reduce this in
|
||||
the case of proportional fonts, at a small cost in performance. The size of the
|
||||
Python source file is a lesser consideration, with readability being prioritised
|
||||
over size. Hence they are "pretty formatted" with the large bytes objects
|
||||
split over multiple lines for readability.
|
||||
|
||||
The approach has been tested on SSD1306 devices using both the pseudo-horizontal
|
||||
and true vertical mapping.
|
||||
|
||||
The ``font_to_py`` utility has been extensively tested with each of the mapping
|
||||
options.
|
|
@ -0,0 +1,97 @@
|
|||
# font_to_py.py
|
||||
|
||||
Convert a font file to Python source code.
|
||||
|
||||
# Usage
|
||||
|
||||
``font_to_py.py`` is a command line utility written in Python 3. It is run on a
|
||||
PC. It takes as input a font file with a ``ttf`` or ``otf`` extension and a
|
||||
required height in pixels and outputs a Python 3 source file. The pixel layout
|
||||
is determined by command arguments. By default fonts are stored in variable
|
||||
pitch form. This may be overidden by a command line argument.
|
||||
|
||||
Further arguments ensure that the byte contents and layout are correct for the
|
||||
target display hardware. Their usage should be specified in the documentation
|
||||
for the device driver.
|
||||
|
||||
Example usage to produce a file ``myfont.py`` with height of 23 pixels:
|
||||
``font_to_py.py FreeSans.ttf 23 myfont.py``
|
||||
|
||||
## Arguments
|
||||
|
||||
### Mandatory positional arguments:
|
||||
|
||||
1. Font file path. Must be a ttf or otf file.
|
||||
2. Height in pixels.
|
||||
3. Output file path. Must have a .py extension.
|
||||
|
||||
### Optional arguments:
|
||||
|
||||
* -f or --fixed If specified, all characters will have the same width. By
|
||||
default fonts are assumed to be variable pitch.
|
||||
* -x Specifies horizontal mapping (default is vertical).
|
||||
* -b Specifies bit reversal in each font byte.
|
||||
|
||||
Optional arguments other than the fixed pitch argument will be specified in the
|
||||
device driver documentation. Bit reversal is required by some display hardware.
|
||||
|
||||
## The font file
|
||||
|
||||
Assume that the you have employed the utility to create a file ``myfont.py``. In
|
||||
your code you will issue
|
||||
|
||||
```python
|
||||
import myfont
|
||||
```
|
||||
|
||||
The ``myfont`` module name will then be passed to the device driver to render
|
||||
strings on demand. The detailed layout of the Python file may be seen [here](./DRIVERS.md).
|
||||
|
||||
# Dependencies, links and licence
|
||||
|
||||
The code is released under the MIT licence. It requires Python 3.2 or later.
|
||||
|
||||
The module relies on [Freetype](https://www.freetype.org/) which is included in most Linux distributions.
|
||||
It uses the [Freetype Python bindings](http://freetype-py.readthedocs.io/en/latest/index.html)
|
||||
which will need to be installed.
|
||||
My solution draws on the excellent example code written by Daniel Bader. This
|
||||
may be viewed [here](https://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python) and [here](https://gist.github.com/dbader/5488053).
|
||||
|
||||
# Appendix: RAM utilisation Test Results
|
||||
|
||||
A font file was created, frozen as bytecode and deployed to a version 1.0
|
||||
Pyboard. The font was saved as variable pitch with a height of 19 pixels. The
|
||||
following code was then pasted at the REPL:
|
||||
|
||||
```python
|
||||
import gc, micropython
|
||||
gc.collect()
|
||||
micropython.mem_info()
|
||||
|
||||
import freeserif
|
||||
|
||||
gc.collect()
|
||||
micropython.mem_info()
|
||||
|
||||
def foo():
|
||||
addr, height, width = freeserif.get_ch('a')
|
||||
|
||||
foo()
|
||||
|
||||
gc.collect()
|
||||
micropython.mem_info()
|
||||
print(len(freeserif._font) + len(freeserif._index))
|
||||
```
|
||||
|
||||
The memory used was 5408, 5648, and 5696 bytes. As increments over the initial
|
||||
state this corresponds to 240 and 288 bytes. The ``print`` statement shows the
|
||||
RAM which would be consumed by the data arrays: this was 3271 bytes.
|
||||
|
||||
The ``foo()`` function emulates the behaviour of a device driver in rendering a
|
||||
character to a display. The local variables constitute memory which will be
|
||||
reclaimed on exit from the function. Its additional RAM use was 48 bytes.
|
||||
|
||||
## Conclusion
|
||||
|
||||
With a font of height 19 pixels RAM saving was an order of magnitude. The
|
||||
saving will be greater if larger fonts are used
|
269
README.md
269
README.md
|
@ -1,21 +1,9 @@
|
|||
# font_to_py.py
|
||||
# MicroPython font handling
|
||||
|
||||
This is a utility written in Python 3 and intended to be run on a PC. It takes
|
||||
as input a font file in ttf or otf form and a height and outputs a Python
|
||||
source file containing the font data. The purpose is to enable font files to be
|
||||
used on microcontrollers running MicroPython: Python source files may be frozen
|
||||
as bytecode. In this form they can be accessed at speed while using very little
|
||||
RAM. The design has the following aims:
|
||||
This is an attempt to offer a standard method of creating and deploying fonts
|
||||
to MicroPython display drivers.
|
||||
|
||||
* Independence of specific display hardware.
|
||||
* The path from font file to Python code to be fully open source.
|
||||
|
||||
The first is achieved by supplying hardware specific arguments to the utility.
|
||||
These define horizontal or vertical mapping and the bit order for font data.
|
||||
|
||||
The second is achieved by using Freetype and the Freetype Python bindings.
|
||||
|
||||
# Rationale
|
||||
# Introduction
|
||||
|
||||
MicroPython platforms generally have limited RAM, but more abundant storage in
|
||||
the form of flash memory. Font files tend to be relatively large. The
|
||||
|
@ -26,16 +14,46 @@ demand is too slow for reasonable performance on most display devices.
|
|||
|
||||
This alternative implements a font as a Python source file, with the data being
|
||||
declared as ``bytes`` objects. Such a file may be frozen as bytecode. On import
|
||||
very little RAM is used, yet the data may be accessed fast.
|
||||
very little RAM is used, yet the data may be accessed fast. Note that the use
|
||||
of frozen bytecode is entirely optional: font files may be imported in the
|
||||
normal way if RAM usage is not an issue.
|
||||
|
||||
It is intended that the resultant file be usable with a variety of display
|
||||
devices and drivers. These include:
|
||||
It is intended that the resultant file be usable with two varieties of display
|
||||
devices and drivers. These comprise:
|
||||
|
||||
1. A driver for the official ``framebuffer`` class.
|
||||
2. Drivers using ``bytearray`` instances as frame buffers.
|
||||
3. Drivers for displays where the frame buffer is implemented in the display
|
||||
1. Drivers using ``bytearray`` instances as frame buffers, including the
|
||||
official ``framebuffer`` class.
|
||||
2. Drivers for displays where the frame buffer is implemented in the display
|
||||
device hardware.
|
||||
|
||||
# The proposed solution
|
||||
|
||||
This consists of three components:
|
||||
|
||||
1. font_to_py.py This is a utility intended to be run on a PC and converts a
|
||||
font file to Python source. See below.
|
||||
2. The Writer class (writer.py) This facilitates writing text to a device
|
||||
given a suitably designed device driver. See [here](./DRIVERS.md).
|
||||
3. A device driver specification. This includes an example for rendering text
|
||||
to an SSD1306 device with arbitrary fonts. See [here](./DRIVERS.md).
|
||||
|
||||
# font_to_py.py
|
||||
|
||||
This is a command line utility written in Python 3 to be run on a PC. It takes
|
||||
as input a font file in ttf or otf form and a height and outputs a Python
|
||||
source file containing the font data. Fixed and variable pitch rendering are
|
||||
supported. The design has the following aims:
|
||||
|
||||
* Independence of specific display hardware.
|
||||
* The path from font file to Python code to be fully open source.
|
||||
|
||||
The first is achieved by supplying hardware specific arguments to the utility.
|
||||
These define horizontal or vertical mapping and the bit order for font data.
|
||||
|
||||
The second is achieved by using Freetype and the Freetype Python bindings. Its
|
||||
use is documented [here](./FONT_TO_PY.md). This also details measurements of
|
||||
RAM usage when importing fonts stored as frozen bytecode.
|
||||
|
||||
# Limitations
|
||||
|
||||
Only the ASCII character set from ``chr(32)`` to ``chr(126)`` is supported.
|
||||
|
@ -43,207 +61,10 @@ Kerning is not supported. Fonts are one bit per pixel. This does not rule out
|
|||
colour displays: the device driver can add colour information at the rendering
|
||||
stage. It does assume that all pixels of a character are rendered identically.
|
||||
|
||||
# Usage
|
||||
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
|
||||
binary font files: these are hand designed for rendering at a specific size.
|
||||
|
||||
``font_to_py.py`` is a command line utility written in Python 3. It is run on a
|
||||
PC. It takes as input a font file with a ``ttf`` or ``otf`` extension and a
|
||||
required height in pixels and outputs a Python 3 source file. The pixel layout
|
||||
is determined by command arguments. By default fonts are stored in variable
|
||||
pitch form. This may be overidden by a command line argument.
|
||||
# Licence
|
||||
|
||||
Further arguments ensure that the byte contents and layout are correct for the
|
||||
target display hardware. Their usage should be specified in the documentation
|
||||
for the device driver.
|
||||
|
||||
Example usage to produce a file ``myfont.py`` with height of 23 pixels:
|
||||
``font_to_py.py FreeSans.ttf 23 myfont.py``
|
||||
|
||||
## Arguments
|
||||
|
||||
### Mandatory positional arguments:
|
||||
|
||||
1. Font file path. Must be a ttf or otf file.
|
||||
2. Height in pixels.
|
||||
3. Output file path. Must have a .py extension.
|
||||
|
||||
### Optional arguments:
|
||||
|
||||
* -f or --fixed If specified, all characters will have the same width. By
|
||||
default fonts are assumed to be variable pitch.
|
||||
* -x Specifies horizontal mapping (default is vertical).
|
||||
* -b Specifies bit reversal in each font byte.
|
||||
|
||||
Optional arguments other than the fixed pitch argument will be specified in the
|
||||
device driver documentation. Bit reversal is required by some display hardware.
|
||||
|
||||
## The font file
|
||||
|
||||
Assume that the you have employed the utility to create a file ``myfont.py``. In
|
||||
your code you will issue
|
||||
|
||||
```python
|
||||
import myfont
|
||||
```
|
||||
|
||||
The ``myfont`` module name will then be passed to the device driver to render
|
||||
strings on demand.
|
||||
|
||||
# Test Results: RAM utilisation
|
||||
|
||||
A font file was created, frozen as bytecode and deployed to a version 1.0
|
||||
Pyboard. The font was saved as variable pitch with a height of 19 pixels. The
|
||||
following code was then pasted at the REPL:
|
||||
|
||||
```python
|
||||
import gc, micropython
|
||||
gc.collect()
|
||||
micropython.mem_info()
|
||||
|
||||
from uctypes import bytearray_at
|
||||
import freeserif
|
||||
|
||||
gc.collect()
|
||||
micropython.mem_info()
|
||||
|
||||
def foo():
|
||||
addr, height, width = freeserif.get_ch('a')
|
||||
arr = bytearray_at(addr, 3*2)
|
||||
|
||||
foo()
|
||||
|
||||
gc.collect()
|
||||
micropython.mem_info()
|
||||
print(len(freeserif._font) + len(freeserif._index))
|
||||
```
|
||||
|
||||
The memory used was 4416, 4704, and 4736 bytes. As increments over the initial
|
||||
state this corresponds to 288 and 320 bytes. The ``print`` statement shows the
|
||||
RAM which would be consumed by the data arrays: this was 3271 bytes.
|
||||
|
||||
The ``foo()`` function emulates the behaviour of a device driver in rendering a
|
||||
character to a display. The local variables constitute memory which will be
|
||||
reclaimed on exit from the function. Its additional RAM use was 32 bytes.
|
||||
|
||||
## Conclusion
|
||||
|
||||
With a font of height 19 pixels RAM saving was an order of magnitude. The
|
||||
saving will be greater if larger fonts are used
|
||||
|
||||
|
||||
# Dependencies, links and licence
|
||||
|
||||
The code is released under the MIT licence. It requires Python 3.2 or later.
|
||||
|
||||
The module relies on [Freetype](https://www.freetype.org/) which is included in most Linux distributions.
|
||||
It uses the [Freetype Python bindings](http://freetype-py.readthedocs.io/en/latest/index.html)
|
||||
which will need to be installed.
|
||||
My solution draws on the excellent example code written by Daniel Bader. This
|
||||
may be viewed [here](https://dbader.org/blog/monochrome-font-rendering-with-freetype-and-python) and [here](https://gist.github.com/dbader/5488053).
|
||||
|
||||
# Implementation
|
||||
|
||||
This section of the README is intended for writers of device drivers.
|
||||
|
||||
## Overview
|
||||
|
||||
The Python source file produced by ``font_to_py.py`` provides a fast means of
|
||||
accessing the byte data corresponding to an individual character. It is the
|
||||
responsibility of the driver to copy that data to the framebuffer or physical
|
||||
device. The purpose of the command line arguments specified to the user is to
|
||||
ensure that the data layout is optimised for the device so that the copy is a
|
||||
simple bytewise copy.
|
||||
|
||||
The user program imports a Python font file. When the user program needs to
|
||||
display a string it passes the module name to the device driver. The module
|
||||
exposes appropriate font metrics (defined in pixels) and a ``get_ch()``
|
||||
function. The latter provides fast access to the bytes corresponding to an
|
||||
individual character together with character specific metrics.
|
||||
|
||||
Fixed width characters include blank bits after the character bits to pad out
|
||||
the width. Variable pitch characters include a small, character specific,
|
||||
number of blank "advance" bits to provide correct spacing between characters.
|
||||
|
||||
## Font files
|
||||
|
||||
Assume the user has run the utility to produce a file ``myfont.py`` This then
|
||||
has the following outline definition (in practice the bytes objects are large):
|
||||
|
||||
```python
|
||||
# Code generated by font-to-py.py.
|
||||
# Font: FreeSerif.ttf
|
||||
version = '0.1'
|
||||
|
||||
def height():
|
||||
return 21
|
||||
|
||||
def max_width():
|
||||
return 22
|
||||
|
||||
def hmap():
|
||||
return False
|
||||
|
||||
def reverse():
|
||||
return False
|
||||
|
||||
def monospaced():
|
||||
return False
|
||||
|
||||
_font =\
|
||||
b'\x06\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'\
|
||||
b'\x00\x00\x00\x00\x08\x00\xfe\xc7\x00\x7e\xc0\x00\x00\x00\x00\x00'\
|
||||
|
||||
_index =\
|
||||
b'\x00\x00\x14\x00\x2e\x00\x4b\x00\x71\x00\x97\x00\xd2\x00\x0a\x01'\
|
||||
b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\
|
||||
|
||||
# Boilerplate code omitted
|
||||
|
||||
def get_ch(ch):
|
||||
# validate ch, if out of range use '?'
|
||||
# get offsets into _font and retrieve char width
|
||||
# Return: address of start of bitmap, height and width
|
||||
return memoryview(_font[offset + 2, next_offset]), height, width
|
||||
```
|
||||
|
||||
``height`` and ``width`` are specified in bits (pixels).
|
||||
|
||||
In the case of monospaced fonts the ``max_width`` function returns the width of
|
||||
every character. For variable pitch fonts it returns the width of the widest
|
||||
character. Device drivers can use this to rapidly determine whether a string
|
||||
will fit the available space. If it will fit on the assumption that all chars
|
||||
are maximum width, it can be rendered rapidly without doing a character by
|
||||
character check.
|
||||
|
||||
``get_ch()`` returns a memoryview of an individual glyph with its dimensions.
|
||||
|
||||
## Mapping
|
||||
|
||||
A character occupies a space where (0, 0) represents the coordinates of the top
|
||||
left hand corner of the bitmap. It comprises a set of pixels where increasing x
|
||||
values represent locations to the right of the origin and increasing y values
|
||||
represent downward positions. Mapping is the process whereby this two
|
||||
dimensional array of bits is transformed into a linear sequence of bytes.
|
||||
|
||||
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
|
||||
(1, 0) to (1, 7).
|
||||
|
||||
Horizontal mapping means that the MSB of byte 0 is pixel (0,0) with LSB at
|
||||
(7,0), with the second byte covering (8, 0) to (15, 0) if the width is greater
|
||||
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].
|
||||
|
||||
# Specification Notes
|
||||
|
||||
The design aims primarily to minimise RAM usage. Minimising the size of the
|
||||
bytecode is a secondary aim. Indexed addressing will be used to reduce this in
|
||||
the case of proportional fonts, at a small cost in performance. The size of the
|
||||
Python source file is a lesser consideration, with readability being prioritised
|
||||
over size. Hence they will be "pretty printed" with the large bytes objects
|
||||
split over multiple lines for readability.
|
||||
|
||||
This general approach has been tested on a Pyboard connected to LCD hardware
|
||||
having an onboard frame buffer. The visual performance is good.
|
||||
All code is released under the MIT licence.
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
# driver_test.py Simple test for rendering text to an ssd1306 display in
|
||||
# arbitrary fonts
|
||||
# V0.1 Peter Hinch Nov 2016
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# This demo uses a modified version of the official SSD1306 driver to fix an
|
||||
# issue with the scroll method. Once the official version is fixed, it should
|
||||
# be substituted for ssd1306_drv
|
||||
|
||||
import machine
|
||||
import utime
|
||||
from writer import Writer
|
||||
from ssd1306_drv import SSD1306_I2C, SSD1306_SPI # Until official module is fixed
|
||||
import freeserif
|
||||
import freesans20
|
||||
import inconsolata16
|
||||
|
||||
# Display parameters
|
||||
WIDTH = const(128)
|
||||
SPI = False
|
||||
# Note that HEIGHT is set below
|
||||
|
||||
if SPI:
|
||||
# Pyb SSD
|
||||
# 3v3 Vin
|
||||
# Gnd Gnd
|
||||
# X1 DC
|
||||
# X2 CS
|
||||
# X3 Rst
|
||||
# X6 CLK
|
||||
# X8 DATA
|
||||
HEIGHT = 32
|
||||
pdc = machine.Pin('X1', machine.Pin.OUT_PP)
|
||||
pcs = machine.Pin('X2', machine.Pin.OUT_PP)
|
||||
prst = machine.Pin('X3', machine.Pin.OUT_PP)
|
||||
spi = machine.SPI(1)
|
||||
display = SSD1306_SPI(WIDTH, HEIGHT, spi, pdc, prst, pcs)
|
||||
else: # I2C
|
||||
# Pyb SSD
|
||||
# 3v3 Vin
|
||||
# Gnd Gnd
|
||||
# Y9 CLK
|
||||
# Y10 DATA
|
||||
HEIGHT = 64
|
||||
pscl = machine.Pin('Y9', machine.Pin.OUT_PP)
|
||||
psda = machine.Pin('Y10', machine.Pin.OUT_PP)
|
||||
i2c = machine.I2C(scl=pscl, sda=psda)
|
||||
display = SSD1306_I2C(WIDTH, HEIGHT, 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')
|
||||
display.show()
|
||||
|
||||
|
||||
def scroll_test(x, y):
|
||||
t = utime.ticks_us()
|
||||
display.scroll(x, y) # 125ms
|
||||
print(utime.ticks_diff(utime.ticks_us(), t))
|
||||
display.show()
|
|
@ -325,25 +325,19 @@ version = '0.1'
|
|||
"""
|
||||
|
||||
STR02 = """
|
||||
try:
|
||||
from uctypes import addressof
|
||||
except ImportError:
|
||||
pass
|
||||
|
||||
def _chr_addr(ordch):
|
||||
offset = 2 * (ordch - 32)
|
||||
return int.from_bytes(_index[offset:offset + 2], 'little')
|
||||
|
||||
def get_ch(ch, test=False):
|
||||
def get_ch(ch):
|
||||
ordch = ord(ch)
|
||||
ordch = ordch if ordch >= 32 and ordch <= 126 else ord('?')
|
||||
offset = _chr_addr(ordch)
|
||||
width = int.from_bytes(_font[offset:offset + 2], 'little')
|
||||
if test:
|
||||
next_offs = _chr_addr(ordch +1)
|
||||
return _font[offset + 2:next_offs], {}, width
|
||||
return addressof(_font) + offset + 2, {}, width
|
||||
|
||||
next_offs = _chr_addr(ordch +1)
|
||||
return memoryview(_font[offset + 2:next_offs]), {}, width
|
||||
|
||||
"""
|
||||
|
||||
def write_func(stream, name, arg):
|
||||
|
|
|
@ -0,0 +1,122 @@
|
|||
# ssd1306_drv.py An implementation of a Display class for SSD1306 based displays
|
||||
# V0.1 Peter Hinch Nov 2016
|
||||
|
||||
# https://learn.adafruit.com/monochrome-oled-breakouts/wiring-128x32-spi-oled-display
|
||||
# https://www.proto-pic.co.uk/monochrome-128x32-oled-graphic-display.html
|
||||
|
||||
# Version supports vertical and "horizontal" modes.
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# SSD1306 classes to fix the official one whose scroll method is broken
|
||||
# This also demonstrates a way to do scrolling for devices with true vertical
|
||||
# mapping (the official driver sets the device into its "horizontal" mode).
|
||||
# This is a hybrid mode where bytes are aligned vertically but arranged
|
||||
# horizontally
|
||||
|
||||
import ssd1306
|
||||
|
||||
class SSD1306(object):
|
||||
def __init__(self, device):
|
||||
self.width = device.width # In pixels
|
||||
self.height = device.height
|
||||
self.bpc = (self.height + 7) >> 3
|
||||
self.device = device
|
||||
self.vmap = False # Currently the official driver uses "horizontal"
|
||||
|
||||
@property
|
||||
def buffer(self): # property emulates underlying device
|
||||
return self.device.buffer
|
||||
|
||||
def show(self):
|
||||
self.device.show()
|
||||
|
||||
# Return map-independent offset into buffer
|
||||
def _offset(self, x, y):
|
||||
if self.vmap:
|
||||
return y + x * self.bpc
|
||||
else:
|
||||
return y * self.width + x
|
||||
|
||||
def scroll(self, x, y):
|
||||
dy = abs(y)
|
||||
if dy:
|
||||
self._scrolly(dy, y > 0)
|
||||
dx = abs(x)
|
||||
if dx:
|
||||
self._scrollx(dx, x > 0)
|
||||
|
||||
def _scrolly(self, ystep, down):
|
||||
buf = self.device.buffer
|
||||
bpc = self.bpc
|
||||
ystep_bytes = (ystep >> 3) + 1
|
||||
ystep_bits = ystep & 7
|
||||
if down:
|
||||
for x in range(self.width):
|
||||
for ydest in range(bpc - 1, -1, -1):
|
||||
ysource = ydest - ystep_bytes
|
||||
data = 0
|
||||
if ysource + 1 >= 0:
|
||||
data = buf[self._offset(x, ysource + 1)] << ystep_bits
|
||||
if ysource >= 0:
|
||||
data |= buf[self._offset(x, ysource)] >> 8 - ystep_bits
|
||||
buf[self._offset(x, ydest)] = data
|
||||
else:
|
||||
for x in range(self.width):
|
||||
for ydest in range(bpc):
|
||||
ysource = ydest + ystep_bytes
|
||||
data = 0
|
||||
if ysource < bpc:
|
||||
data = buf[self._offset(x, ysource)] << (8-ystep_bits)
|
||||
if ysource - 1 < bpc:
|
||||
data |= buf[self._offset(x, ysource - 1)] >> ystep_bits
|
||||
buf[self._offset(x, ydest)] = data
|
||||
|
||||
def _scrollx(self, xstep, right): # scroll x
|
||||
buf = self.device.buffer
|
||||
bpc = self.bpc
|
||||
for y in range(bpc):
|
||||
if right: # Scroll right
|
||||
for xdest in range(self.width - 1, -1, -1):
|
||||
data = 0
|
||||
xsource = xdest - xstep
|
||||
if xsource >= 0:
|
||||
data = buf[self._offset(xsource, y)]
|
||||
buf[self._offset(xdest, y)] = data
|
||||
else:
|
||||
for xdest in range(0, self.width):
|
||||
data = 0
|
||||
xsource = xdest + xstep
|
||||
if xsource < self.width:
|
||||
data = buf[self._offset(xsource, y)]
|
||||
buf[self._offset(xdest, y)] = data
|
||||
|
||||
class SSD1306_I2C(SSD1306):
|
||||
def __init__(self, width, height, i2c, addr=0x3c, external_vcc=False):
|
||||
device = ssd1306.SSD1306_I2C(width, height, i2c, addr, external_vcc)
|
||||
super().__init__(device)
|
||||
|
||||
class SSD1306_SPI(SSD1306):
|
||||
def __init__(self, width, height, spi, dc, res, cs, external_vcc=False):
|
||||
device = ssd1306.SSD1306_SPI(width, height, spi, dc, res, cs, external_vcc)
|
||||
super().__init__(device)
|
|
@ -0,0 +1,123 @@
|
|||
# writer.py Implements the Writer class.
|
||||
# V0.1 Peter Hinch Nov 2016
|
||||
|
||||
# The MIT License (MIT)
|
||||
#
|
||||
# Copyright (c) 2016 Peter Hinch
|
||||
#
|
||||
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
# of this software and associated documentation files (the "Software"), to deal
|
||||
# in the Software without restriction, including without limitation the rights
|
||||
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
# copies of the Software, and to permit persons to whom the Software is
|
||||
# furnished to do so, subject to the following conditions:
|
||||
#
|
||||
# The above copyright notice and this permission notice shall be included in
|
||||
# all copies or substantial portions of the Software.
|
||||
#
|
||||
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
# THE SOFTWARE.
|
||||
|
||||
# A Writer supports rendering text to a Display instance in a given font.
|
||||
# Currently supports vertical mapping and the SSD1306 "pseudo-horizontal" map
|
||||
# only
|
||||
# Multiple Writer instances may be created, each rendering a font to the
|
||||
# same Display object.
|
||||
|
||||
VERT = 0
|
||||
HORIZ = 1
|
||||
WEIRD = 2
|
||||
|
||||
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
|
||||
bmap = WEIRD # SSD1306 pseudo-horizontal
|
||||
|
||||
@classmethod
|
||||
def set_textpos(cls, row, col):
|
||||
cls.text_row = row
|
||||
cls.text_col = col
|
||||
|
||||
@classmethod
|
||||
def set_clip(cls, row_clip, col_clip):
|
||||
cls.row_clip = row_clip
|
||||
cls.col_clip = col_clip
|
||||
|
||||
@classmethod
|
||||
def mapping(cls, bmap):
|
||||
if bmap in (VERT, HORIZ):
|
||||
cls.bmap = bmap
|
||||
else:
|
||||
raise ValueError('Unsupported mapping')
|
||||
|
||||
def __init__(self, device, font):
|
||||
super().__init__()
|
||||
self.device = device
|
||||
self.font = font
|
||||
if font.hmap():
|
||||
raise OSError('Font must be vertically mapped')
|
||||
self.screenwidth = device.width # In pixels
|
||||
self.screenheight = device.height
|
||||
div, mod = divmod(device.height, 8)
|
||||
self.bytes_per_col = div + 1 if mod else div
|
||||
|
||||
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
|
||||
|
||||
def printstring(self, string):
|
||||
for char in string:
|
||||
self._printchar(char)
|
||||
|
||||
def _printchar(self, char):
|
||||
bmap = Writer.bmap # Buffer mapping
|
||||
if char == '\n':
|
||||
self._newline()
|
||||
return
|
||||
fbuff = self.device.buffer
|
||||
glyph, char_height, char_width = self.font.get_ch(char)
|
||||
if Writer.text_row+char_height > self.screenheight and Writer.row_clip:
|
||||
return
|
||||
if Writer.text_col + char_width > self.screenwidth:
|
||||
if Writer.col_clip:
|
||||
return
|
||||
else:
|
||||
self._newline()
|
||||
div, mod = divmod(char_height, 8)
|
||||
gbytes = div + 1 if mod else div # No. of bytes per column of glyph
|
||||
start_row, align = divmod(Writer.text_row, 8)
|
||||
for scol in range(0, char_width): # Source column
|
||||
dcol = scol + Writer.text_col # Destination column
|
||||
dest_row_byte = start_row
|
||||
for gbyte in range(gbytes): # Each glyph byte in column
|
||||
if bmap == VERT:
|
||||
dest = dcol * self.bytes_per_col + dest_row_byte
|
||||
elif bmap == WEIRD:
|
||||
dest = dcol + dest_row_byte * self.screenwidth
|
||||
source = scol * gbytes + gbyte
|
||||
data = fbuff[dest] & (0xff >> (8 - align))
|
||||
fbuff[dest] = data | glyph[source] << align
|
||||
if align and (dest_row_byte + 1) < self.bytes_per_col:
|
||||
if bmap == VERT:
|
||||
dest += 1
|
||||
elif bmap == WEIRD:
|
||||
dest += self.screenwidth
|
||||
data = fbuff[dest] & (0xff << align)
|
||||
fbuff[dest] = data | glyph[source] >> (8 - align)
|
||||
dest_row_byte += 1
|
||||
if dest_row_byte >= self.bytes_per_col:
|
||||
break
|
||||
Writer.text_col += char_width
|
Ładowanie…
Reference in New Issue