diff --git a/FONT_TO_PY.md b/FONT_TO_PY.md index db4519d..6570d5b 100644 --- a/FONT_TO_PY.md +++ b/FONT_TO_PY.md @@ -27,7 +27,8 @@ pitch form. This may be overidden by a command line argument. By default the ASCII character set (ordinal values 32 to 126 inclusive) is supported. Command line arguments can modify this range as required, if -necessary to include extended ASCII characters up to 255. +necessary to include extended ASCII characters up to 255. Alternatively non +English or non-contiguous character sets may be defined. Further arguments ensure that the byte contents and layout are correct for the target display hardware. Their usage should be specified in the documentation @@ -61,27 +62,28 @@ Example usage to produce a file `myfont.py` with height of 23 pixels: for alternative character sets such as Cyrillic: the file must contain the character set to be included. An example file is `cyrillic`. -The -c option reduces the size of the font file. If the font file is frozen as -bytecode this will not reduce RAM usage but it will conserve flash. An example -usage for a digital clock font: +The -c option may be used to reduce the size of the font file by limiting the +character set. If the font file is frozen as bytecode this will not reduce RAM +usage but it will conserve flash. Example usage for a digital clock font: ```shell $ font_to_py.py Arial.ttf 20 arial_clock.py -c 1234567890: ``` Example usage with the -k option: ```shell -font_to_py.py FreeSans.ttf 20 freesans_cyr_20.py -k cyrillic_subset +font_to_py.py FreeSans.ttf 20 freesans_cyr_20.py -k cyrillic ``` If a character set is specified, `--smallest` and `--largest` should not be -specified: these values are computed from the charcater set. +specified: these values are computed from the character set. Any requirement for arguments -xr will be specified in the device driver documentation. Bit reversal is required by some display hardware. There have been reports that producing extended ASCII characters (ordinal value > 127) from ttf files is unreliable. If the expected results are not -achieved, use an otf font. This may be a limitation of the `freetype` library. +achieved, use an otf font. However I have successfully created the Cyrillic +font from a `ttf`. Perhaps not all fonts are created equal... ### Output @@ -128,13 +130,15 @@ Consequently the following arguments are invalid: # Dependencies, links and licence -The code is released under the MIT licence. It requires Python 3.2 or later. +The code is released under the MIT licence. The `font_to_py.py` utility +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). +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 diff --git a/README.md b/README.md index 49befa8..94885ab 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # MicroPython font handling This repository defines a method of creating and deploying fonts for use with -MicroPython display drivers. A PC utility converts industry standard font files -to Python sourcecode and a MicroPython module enables these to be rendered to -suitable device drivers, notably OLED displays using the SSD1306 chip. +MicroPython display drivers. A PC utility renders industry standard font files +as a bitmap in the form of Python sourcecode. A MicroPython module enables such +files to be displayed on devices with suitable device drivers. These include +OLED displays using the SSD1306 chip and the official device driver. # Introduction @@ -34,8 +35,8 @@ This comprises three components: 1. [font_to_py.py](./FONT_TO_PY.md) This utility runs on a PC and converts a font file to Python source. See below. - 2. [The Writer class](./writer/WRITER.md) This facilitates rendering text to a - device having a suitably designed device driver. + 2. [Writer and CWriter classes](./writer/WRITER.md) These facilitate rendering + text to a monochrome or colour display having a suitable device driver. 3. [Device driver notes](./writer/DRIVERS.md). Notes for authors of display device drivers. Provides details of the font file format and information on ensuring comptibility with the `Writer` classes. @@ -59,28 +60,31 @@ RAM usage when importing fonts stored as frozen bytecode. # Limitations -By default the ASCII character set from `chr(32)` to `chr(126)` is supported -but command line arguments enable the range to be modified with extended ASCII -characters to `chr(255)` being included if required. 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. +Kerning is not supported. Fonts are one bit per pixel. Colour displays are +supported by the `CWriter` class which adds colour information at the rendering +stage. This assumes that all pixels of a character are coloured 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 size. +By default the `font_to_py.py` utility produces the ASCII character set from +`chr(32)` to `chr(126)` inclusive. Command line options enable the character +set to be modified to include extended ASCII. Alternative sets may be specified +such as non-English languages or limited, non-contiguous sets for specialist +applications. + # Font file interface A font file is imported in the usual way e.g. `import font14`. It contains the following methods which return values defined by the arguments which were -provided to font-to-py: +provided to `font_to_py.py`: `height` Returns height in pixels. `max_width` Returns maximum width of a glyph in pixels. -`hmap` Returns `True` if font is horizontally mapped. Should return `True` -`reverse` Returns `True` if bit reversal was specified. Should return `False` +`hmap` Returns `True` if font is horizontally mapped. +`reverse` Returns `True` if bit reversal was specified. `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. @@ -92,6 +96,12 @@ and it returns the following values: * The height in pixels. * The character width in pixels. +The `font_to_py.py` utility allows a default glyph to be specified (typically +`?`). If called with an undefined character, this glyph will be returned. + +The `min_ch` and `max_ch` methods are mainly relevant to contiguous character +sets. + # Licence All code is released under the MIT licence. diff --git a/writer/DRIVERS.md b/writer/DRIVERS.md index 6ff53ee..8cd661c 100644 --- a/writer/DRIVERS.md +++ b/writer/DRIVERS.md @@ -1,33 +1,50 @@ # 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 +includes a frame buffer or whether a frame buffer must be located on the controlling system. -In the latter case the [Writer](./WRITER.md) class extends the capability of -the driver to use multiple fonts plus additional functionality. +If the device has no frame buffer then the device driver should be designed +to subclass `framebuf.FrameBuffer` with a suitably sized buffer on the host. If +the device has its own frame buffer there are two options for the driver. One +is to perform all display operations using the device's own firmware +primitives. This is efficient and avoids the need for a buffer on the host, +however it does involve some code complexity. -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. +The second option is to subclass `framebuf.FrameBuffer`, provide a buffer on +the host, and copy its contents to the device's buffer when required. This can +result in a very simple device driver at cost of RAM use and update speed. It +also ensures compatibility with additional libraries to simplify display tasks. + +If a device subclasses `framebuf.FrameBuffer` the following libraries enhance +its capability. The [Writer](./WRITER.md) class enables it to use multiple +fonts with additional functionality such as word wrap, string metrics and tab +handling. The [nano-gui](https://github.com/peterhinch/micropython-nano-gui.git) +provides rudimentary GUI capability. + +If a driver relies on a buffer located on the display device, the means of +controlling the text insertion point, performing partial buffer updates and +executing graphics primitives will be device dependent. If the functionality of +the `writer` or `nanogui` libraries are required it will need to be +implemented at device driver level. ###### [Main README](../README.md) -## Drivers for unbuffered displays +# Drivers subclassed from framebuf 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](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). +subclassed from the official `framebuf.FrameBuffer` class. An example of such a +driver is the [official SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). 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. +containing the size of the display in pixels, plus a `show` method which copies +the buffer to the physical device. 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. +Monochrome displays should define the frame buffer format to match the physical +characteristics of the display. In the case of colour displays RAM may be saved +by using `framebuf.GS8` 8-bit colour. The `show` method can map this to the +device's colour space if 8-bit mode is not supported. 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 @@ -37,14 +54,26 @@ 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 +## Example drivers + +The following drivers are subclassed from `framebuf.FrameBuffer` and have been +tested with `writer.py` and `nanogui.py`. + + * The [SSD1306 OLED driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py) + * The [Nokia 5110](https://github.com/mcauser/micropython-pcd8544/blob/master/pcd8544_fb.py) + * The [SSD1331 colour OLED](https://github.com/peterhinch/micropython-nano-gui/blob/master/drivers/ssd1331/ssd1331.py) + +The latter example illustrates a very simple driver which provides full access +to `writer.py` and `nanogui.py` libraries. + +# Drivers using the display buffer Authors of such drivers will need to have an understanding of the font file format. -### Specifying the font layout +## Specifying the font layout -Each font file has a `get_ch()` function accepting an ASCII character as its +Each font file has a `get_ch()` function accepting a 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 @@ -196,10 +225,11 @@ i.e. a byte comprising bits [b7b6b5b4b3b2b1b0] becomes [b0b1b2b3b4b5b6b7]. 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 case of proportional fonts, and also to facilitate non-contiguous 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, diff --git a/writer/WRITER.md b/writer/WRITER.md index 8d92d2c..54df6ff 100644 --- a/writer/WRITER.md +++ b/writer/WRITER.md @@ -1,12 +1,17 @@ # Writer and Cwriter classes These classes facilitate rendering Python font files to displays where the -display driver is subclassed from the `framebuf` class. An example is the -official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). +display driver is subclassed from the `framebuf` class. Examples are: + + * The official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). + * The [PCD8544/Nokia 5110](https://github.com/mcauser/micropython-pcd8544.git). + * The [Adafruit 0.96 inch color OLED](https://www.adafruit.com/product/684) + with [this driver](https://github.com/peterhinch/micropython-nano-gui/tree/master/drivers/ssd1331). Basic support is for scrolling text display using multiple fonts. The -`writer_gui` module provides optional extensions for user interface objects -displayed at arbitrary locations on screen. +[nanogui](https://github.com/peterhinch/micropython-nano-gui.git) module has +optional extensions for user interface objects displayed at arbitrary locations +on screen. Example code and images are for 128*64 SSD1306 OLED displays. @@ -23,7 +28,7 @@ Right justified text. Mixed text and graphics. ![Image](images/fields.JPG) -Labels and Fields (from writer_gui.py). +Labels and Fields (from nanogui.py). # Contents @@ -41,9 +46,7 @@ Labels and Fields (from writer_gui.py). 2.2.1 [Static Method](./WRITER.md#221-static-method) 2.2.2 [Constructor](./WRITER.md#222-constructor) 2.2.3 [Methods](./WRITER.md#223-methods) - 3. [The writer_gui module](./WRITER.md#3-the-writer_gui-module) - 3.1 [The Label class](./WRITER.md#31-the-label-class) - 4. [Notes](./WRITER.md#4-notes) + 3. [Notes](./WRITER.md#4-notes) ###### [Main README](../README.md) @@ -61,17 +64,19 @@ This update for practical applications has the following features: * Tab support. * String metrics to enable right or centre justification. * Inverse (background color on foreground color) display. - * Labels: render static text at a fixed location. - * Fields - render dynamically changing text to a fixed rectangular region. * Inverted display option. +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`. + ## 1.1 Hardware Tests and demos assume a 128*64 SSD1306 OLED display connected via I2C or SPI. Wiring is specified in `ssd1306_setup.py`. Edit this to use a different bus or for a non-Pyboard target. At the time of writing the default of software I2C should be used: the official SSD1306 driver is not compatible with hardware I2C -(see [Notes](./WRITER.md#4-notes)). +(see [Notes](./WRITER.md#3-notes)). ## 1.2 Files @@ -240,58 +245,7 @@ The `printstring` method works as per the base class except that the string is rendered in foreground color on background color (or reversed if `invert` is `True`). -# 3. The writer_gui module - -This supports user interface objects whose text components are drawn using the -`Writer` or `CWriter` classes. Upside down rendering is not supported: attempts -to specify it will produce unexpected results. - -The objects are drawn at specific locations on screen and are incompatible with -the display of scrolling text: they are therefore not intended for use with the -writer's `printstring` method. - -## 3.1 The Label class - -This supports applications where text is to be rendered at specific screen -locations. - -Text can be static or dynamic. In the case of dynamic text the background is -cleared to ensure that short strings can cleanly replace longer ones. - -Labels can be displayed with an optional single pixel border. - -Colors are handled flexibly. By default the colors used are those of the -`Writer` instance, however they can be changed dynamically, for example to warn -of overrange values. - -Constructor args: - 1. `writer` The `Writer` instance (font and screen) to use. - 2. `row` Location on screen. - 3. `col` - 4. `text` If a string is passed it is displayed: typically used for static - text. If an integer is passed it is interpreted as the maximum text length - in pixels; typically obtained from `writer.stringlen('-99.99')`. Nothing is - dsplayed until `.value()` is called. Intended for dynamic text fields. - 5. `invert=False` Display in inverted or normal style. - 6. `fgcolor=None` Optionally override the `Writer` colors. - 7. `bgcolor=None` - 8. `bordercolor=False` If `False` no border is displayed. If `None` a border - is shown in the `Writer` forgeround color. If a color is passed, it is used. - -The constructor displays the string at the required location. - -Methods: - 1. `value` Redraws the label. This takes the following args: - 1. `text=None` The text to display. If `None` displays last value. - 2. ` invert=False` If true, show inverse text. - 3. `fgcolor=None` Foreground color: if `None` the `Writer` default is used. - 4. `bgcolor=None` Background color, as per foreground. - 5. `bordercolor=None` As per above except that if `False` is passed, no - border is displayed. This clears a previously drawn border. - Returns the current text string. - 2. `show` No args. (Re)draws the label. For future/subclass use. - -# 4. Notes +# 3. Notes Possible future enhancements: 1. General rendering to a rectangular area. This may be problematic as the diff --git a/writer/writer.py b/writer/writer.py index 7684354..69d2e1c 100644 --- a/writer/writer.py +++ b/writer/writer.py @@ -102,7 +102,7 @@ class Writer(): def _newline(self): s = self._getstate() - height = self.height() + height = self.font.height() if self.usd: s.text_row -= height s.text_col = self.screenwidth - 1 @@ -130,7 +130,8 @@ class Writer(): self.wrap = wrap return self.row_clip, self.col_clip, self.wrap - def height(self): + @property + def height(self): # Property for consistency with device return self.font.height() def printstring(self, string, invert=False): diff --git a/writer/writer_demo.py b/writer/writer_demo.py index 4ee79e5..ae0a32a 100644 --- a/writer/writer_demo.py +++ b/writer/writer_demo.py @@ -1,8 +1,10 @@ -# ssd1306_test.py Demo pogram for rendering arbitrary fonts to an SSD1306 OLED display. +# writer_demo.py Demo pogram for rendering arbitrary fonts to an SSD1306 OLED display. +# Illustrates a minimal example. Requires ssd1306_setup.py which contains +# wiring details. # The MIT License (MIT) # -# Copyright (c) 2016 Peter Hinch +# Copyright (c) 2018 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 diff --git a/writer/writer_tests.py b/writer/writer_tests.py index 1979461..f7e5220 100644 --- a/writer/writer_tests.py +++ b/writer/writer_tests.py @@ -1,8 +1,8 @@ -# ssd1306_test.py Demo pogram for rendering arbitrary fonts to an SSD1306 OLED display. +# ssd1306_test.py Demo program for rendering arbitrary fonts to an SSD1306 OLED display. # The MIT License (MIT) # -# Copyright (c) 2016 Peter Hinch +# Copyright (c) 2018 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 @@ -33,12 +33,13 @@ import utime import uos from ssd1306_setup import WIDTH, HEIGHT, setup from writer import Writer, CWriter -from writer_gui import Label +from writer_gui import Label, Meter # Fonts import freesans20 import courier20 as fixed import font6 as small +import arial10 def inverse(use_spi=False, soft=True): ssd = setup(use_spi, soft) # Create a display instance @@ -175,50 +176,6 @@ def wrap(use_spi=False, soft=True): wri.printstring('the quick brown fox jumps over') ssd.show() -def fields(use_spi=False, soft=True): - ssd = setup(use_spi, soft) # Create a display instance - Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it - wri = Writer(ssd, fixed, verbose=False) - wri.set_clip(False, False, False) - textfield = Label(wri, 0, 2, wri.stringlen('longer')) - numfield = Label(wri, 25, 2, wri.stringlen('99.99'), bordercolor=None) - countfield = Label(wri, 0, 90, wri.stringlen('1')) - n = 1 - for s in ('short', 'longer', '1', ''): - textfield.value(s) - numfield.value('{:5.2f}'.format(int.from_bytes(uos.urandom(2),'little')/1000)) - countfield.value('{:1d}'.format(n)) - n += 1 - ssd.show() - utime.sleep(2) - textfield.value('Done', True) - ssd.show() - -def multi_fields(use_spi=False, soft=True): - ssd = setup(use_spi, soft) # Create a display instance - Writer.set_textpos(ssd, 0, 0) # In case previous tests have altered it - wri = Writer(ssd, small, verbose=False) - wri.set_clip(False, False, False) - - nfields = [] - dy = small.height() + 6 - y = 2 - col = 15 - width = wri.stringlen('99.99') - for txt in ('X:', 'Y:', 'Z:'): - Label(wri, y, 0, txt) - nfields.append(Label(wri, y, col, width, bordercolor=None)) # Draw border - y += dy - - for _ in range(10): - for field in nfields: - value = int.from_bytes(uos.urandom(3),'little')/167722 - field.value('{:5.2f}'.format(value)) - ssd.show() - utime.sleep(1) - Label(wri, 0, 64, ' DONE ', True) - ssd.show() - def dual(use_spi=False, soft=True): ssd0 = setup(False, soft) # I2C display ssd1 = setup(True, False) # SPI instance @@ -243,7 +200,7 @@ def dual(use_spi=False, soft=True): for _ in range(10): for n, wri in enumerate((wri0, wri1)): for field in nfields[n]: - value = int.from_bytes(uos.urandom(3),'little')/167722 + value = int.from_bytes(uos.urandom(3),'little')/167772 field.value('{:5.2f}'.format(value)) wri.device.show() utime.sleep(1) @@ -268,8 +225,6 @@ fonts() Two fonts. tabs() Tab stops. usd_tabs() Upside-down tabs. wrap() Word wrapping -fields() Label test with dynamic data. -multi_fields() More Labels. dual() Test two displays on one host.''' print(tstr)