micropython-font-to-py/writer/DRIVERS.md

8.6 KiB

Device Driver Implementation

Display devices comprise two varieties, depending on whether the hardware includes a frame buffer or whether the frame buffer is located on the controlling system.

In the latter case the Writer class extends the capability of the driver to use multiple fonts plus additional functionality.

Where the buffer is located on the display device, the means of controlling the text insertion point and the means of performing partial buffer updates will be device dependent. If the functionality of the Writer class is required it must be implemented at device driver level.

Main README

Drivers for unbuffered displays

Where the buffer is held on the MicroPython host the driver should be subclassed from the official framebuf module. An example of such a driver is the official SSD1306 driver. In addition the driver class should have bound variables width and height containing the size of the display in pixels. Color displays should have a bound variable mode holding the framebuf color mode.

The device driver defines a buffer of the correct size to hold a full frame of data and instantiates the framebuf.FrameBuffer superclass to reference it. The FrameBuffer mode is selected to match the layout of the target display. The driver implements a show method to efficiently copy the buffer contents to the display hardware.

This design enables the supplied Writer and CWriter classes to be used for rendering arbitrary fonts to the display. The author of the device driver need not be concerned with the format of Python font files.

The Writer and CWriter classes require horizontally mapped fonts. This is regardless of the mapping used in the device driver's FrameBuffer: the Writer.printstring method deals transparently with any mismatch.

Drivers for buffered displays

Authors of such drivers will need to have an understanding of the font file format.

Specifying the font layout

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. It is the responsibility of the driver to copy that data to the physical device.

The purpose of the font_to_py.py command line arguments specified to the user is to ensure that the data layout is optimised for the device so that this copy operation is a fast bytewise copy or SPI/I2C transfer. The driver documentation should therefore specify these arguments to ensure the layout is optimal. Mapping may be horizontal or vertical, and the bit order of individual bytes may be defined. These are detailed below.

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. Consideration should be given to employing the same interface as the Writer class to simplify the porting of user code between displays with differing hardware.

Python 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):

# Code generated by font-to-py.py.
# Font: Arial.ttf
version = '0.25'

def height():
    return 20

def max_width():
    return 20

def hmap():
    return False

def reverse():
    return False

def monospaced():
    return False

def min_ch():
    return 32

def max_ch():
    return 126

_font =\
b'\x0b\x00\x18\x00\x00\x1c\x00\x00\x0e\x00\x00\x06\xce\x00\x06\xcf'\
b'\x00\x86\x03\x00\xce\x01\x00\xfc\x00\x00\x38\x00\x00\x00\x00\x00'\

_index =\
b'\x00\x00\x23\x00\x23\x00\x37\x00\x37\x00\x4b\x00\x4b\x00\x62\x00'\
b'\x62\x00\x85\x00\x85\x00\xa8\x00\xa8\x00\xe0\x00\xe0\x00\x09\x01'\

_mvfont = memoryview(_font)

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

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.

The _font bytearray holds the glyphs corresponding to every character in the font. Entry 0 is the default glyph, used if an attempt is made to render a nonexistent character.

The index holds two integers (each occupying 2 bytes) per character. The index has an entry for every character in the specified range, whether or not that character exists.

Index entries are offsets into the _font bytearray represnting the start and end of the glyph. If the font comprises a set of characters which is not contiguous, missing characters have an index entry which points to the first glyph in the _font bytearray. This ensures that the default glyph is rendered.

Fixed width fonts

If a Python font file is created with the -f argument, all characters will be saved with the width of the widest. In general it is not necessary to specify this option. The driver can perform fixed pich rendering by rendering the character as variable pitch, then blanking and advancing the pixel column by the value returned by font.max_width().

Binary font files

This format is unlikely to find application beyond the e-paper driver. It was designed for micropower applications where the Pyboard has no SD card. Fonts are stored as random access files on power-switched Flash storage or SD card. This method is probably too slow for anything other than e-paper displays.

The format is as follows. Files are binary with a four byte header and 126 fixed length records. The header consists of two file identifiers enabling the file format to be checked, followed by bytes specifying the width and height. The length of each record is (width + 1) bytes.

The file indentifiers depend on the -x and -r arguments specified to font_to_py.py and are as follows:

hmap reverse byte
-x -r 0 1
0 0 0x3f 0xe7
1 0 0x40 0xe7
0 1 0x41 0xe7
1 1 0x42 0xe7

Each record starts with a width byte specifying the x dimension of the glyph if rendered proportionally spaced, followed by the glyph data. This data includes trailing space ensuring that all records have the size specified in the header.

Mapping (Python and Binary fonts)

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 defines the relationship between this abstract two dimensional array of bits and the physical 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.

Fonts created with the font_to_py utility have been extensively tested with each of the mapping options. They are used with drivers for SSD1306 OLEDs, SSD1963 LCD displays, the official LCD160CR and the Digital Artists 2.7 inch e-paper display.