diff --git a/writer/WRITER.md b/writer/WRITER.md index ed65edf..5cdcb68 100644 --- a/writer/WRITER.md +++ b/writer/WRITER.md @@ -48,30 +48,31 @@ Labels and Fields (from nanogui.py). # Contents 1. [Introduction](./WRITER.md#1-introduction) - 1.1 [Hardware](./WRITER.md#11-hardware) - 1.2 [Files](./WRITER.md#11-files) - 1.3 [Fonts](./WRITER.md#11-fonts) + 1.1 [Release notes](./WRITER.md#11-release-notes) + 1.2 [Hardware](./WRITER.md#12-hardware) + 1.3 [Files](./WRITER.md#13-files) + 1.4 [Fonts](./WRITER.md#14-fonts) 2. [Writer and CWriter classes](./WRITER.md#2-writer-and-cwriter-classes) 2.1 [The Writer class](./WRITER.md#21-the-writer-class) For monochrome displays.      2.1.1 [Static Method](./WRITER.md#211-static-method)      2.1.2.[Constructor](./WRITER.md#212-constructor)      2.1.3 [Methods](./WRITER.md#213-methods) - 2.2 [The CWriter class](./WRITER.md#22-the-cwriter-class) For colour displays - and for upside-down rendering. -      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) -      2.2.4 [A performance boost](./WRITER.md#224-a-performance-boost) - 3. [Notes](./WRITER.md#4-notes) + 2.2 [The CWriter class](./WRITER.md#22-the-cwriter-class) For colour displays. +      2.2.1 [Constructor](./WRITER.md#221-constructor) +      2.2.2 [Methods](./WRITER.md#222-methods) +      2.2.3 [A performance boost](./WRITER.md#223-a-performance-boost) + 3. [Notes](./WRITER.md#3-notes) ###### [Main README](../README.md) # 1. Introduction -The original `Writer` class was a proof of concept intended to demonstrate -rendering, on an SSD1306 OLED display, fonts created by`font_to_py.py`. +The module provides a `Writer` class for rendering bitmapped monochrome fonts +created by `font_to_py.py`. The `CWriter` class extends this to support color +rendering. Rendering is to a `FrameBuffer` instance, e.g. to a display whose +driver is subclassed from a `FrameBuffer`. -This update for practical applications has the following features: +The module has the following features: * Genarality: capable of working with any `framebuf` derived driver. * Multiple display operation. * Text display of fixed and variable pitch fonts with wrapping and vertical @@ -80,29 +81,42 @@ 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. - * 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 +## 1.1 Release Notes + +V0.4.0 Jan 2021 +Improved handling of the `col_clip` and `wrap` options. Improved accuracy +avoids needless word wrapping. The clip option now displays as much of the last +visible glyph as possible: formerly a glyph which would not fit in its entirety +was discarded. + +The inverted display option has been withdrawn. It added significant code size +and was not an optimal solution. Display inversion should be done at the device +driver level. Such a solution works for graphics objects and GUI widgets, while +the old option only affected rendered text. + +## 1.2 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. -## 1.2 Files +## 1.3 Files 1. `writer.py` Supports `Writer` and `CWriter` classes. - 2. `writer_gui.py` Provides optional GUI objects. - 3. `ssd1306_setup.py` Hardware initialisation for SSD1306. Requires the + 2. `ssd1306_setup.py` Hardware initialisation for SSD1306. Requires the official [SSD1306 driver](https://github.com/micropython/micropython/blob/master/drivers/display/ssd1306.py). - 4. `writer_demo.py` Demo using a 128*64 SSD1306 OLED display. Import to see + 3. `writer_demo.py` Demo using a 128*64 SSD1306 OLED display. Import to see usage information. - 5. `writer_tests.py` Test/demo scripts. Import to see usage information. - 6. `writer_minimal.py` A minimal version for highly resource constrained + 4. `writer_tests.py` Test/demo scripts. Import to see usage information. + 5. `writer_minimal.py` A minimal version for highly resource constrained devices. + 6. `framebuf_utils.framebuf_utils.mpy` A means of improving rendering speed + on color displays. Discussed [in 2.2.3](./WRITER.md#223-a-performance-boost) Sample fonts: 1. `freesans20.py` Variable pitch font file. @@ -110,25 +124,24 @@ Sample fonts: 3. `font10.py` Smaller variable pitch fonts. 4. `font6.py` -## 1.3 Fonts +## 1.4 Fonts Python font files should be created using `font-to-py.py` using horizontal mapping (`-x` option). The `-r` option is not required. If RAM is critical fonts may be frozen as bytecode reducing the RAM impact of each font to about -340 bytes. +340 bytes. This is highly recommended. ###### [Contents](./WRITER.md#contents) # 2. Writer and CWriter classes The `Writer` class provides fast rendering to monochrome displays using bit -blitting. Most applications will use this class. +blitting. -The `CWriter` class is a subclass of `Writer`. It can optionally support color -displays. It provides additional functionality in the form of an upside-down -display option. Owing to limitations in the `frmebuf.blit` method the -`CWriter` class renders glyphs one pixel at a time; rendering is therefore -slower than the `Writer` class. +The `CWriter` class is a subclass of `Writer` to support color displays. Owing +to limitations in the `frmebuf.blit` method the `CWriter` class renders glyphs +one pixel at a time; rendering is therefore slower than the `Writer` class. A +substantial improvement is possible. See [2.2.3](./WRITER.md#223-a-performance-boost). 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 @@ -176,10 +189,10 @@ SSD1306 OLED display and the official The `Writer` class exposes the following static method: - 1. `set_textpos` Args: `device`,`row=None`, `col=None`. The `device` is the - display instance. This method determines where on screen 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. The + 1. `set_textpos(device, row=None, col=None)`. The `device` is the display + instance. This method determines where on screen 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. The insertion point defines the top left hand corner of the next character to be output. @@ -199,45 +212,37 @@ This takes the following args: ### 2.1.3 Methods - 1. `printstring` Args: `string`, `invert=False`. Outputs a text string at the - current insertion point. Newline and Tab characters are honoured. If `invert` - is `True` the text is output as black on white. - 2. `height` No args. Returns the font height in pixels. - 3. `stringlen` Arg: `string`. Returns the length of a string in pixels. Used - for right or centre justification. - 4. `set_clip` Args: `row_clip=None`, `col_clip=None`, `wrap=None`. If - `row_clip` and/or `col_clip` 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. If - `wrap` is `True` word-wrapping will be performed, assuming words are separated - by spaces. + 1. `printstring(string, invert=False)`. Renders the string at the current + insertion point. Newline and Tab characters are honoured. If `invert` is + `True` the text is output with foreground and background colors transposed. + 2. `height()` Returns the font height in pixels. + 3. `stringlen(string, oh=False)` Returns the length of a string in pixels. + Appications can use this for right or centre justification. + The `oh` arg is for internal use. If set, the method returns a `bool`, `True` + if the string would overhang the display edge if rendered at the current + insertion point. + 4. `set_clip(row_clip=None, col_clip=None, wrap=None)`. If `row_clip` and/or + `col_clip` 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. If `wrap` is `True` + word-wrapping will be performed, assuming words are separated by spaces. If any arg is `None`, that value will be left unchanged. Returns the current values of `row_clip`, `col_clip` and `wrap`. - 5. `tabsize` Arg `value=None`. If `value` is an integer sets the tab size. - Returns the current tab size (initial default is 4). Tabs only work properly - with fixed pitch fonts. + 5. `tabsize(value=None)`. If `value` is an integer sets the tab size. Returns + the current tab size (initial default is 4). Tabs only work properly with + fixed pitch fonts. ###### [Contents](./WRITER.md#contents) ## 2.2 The CWriter class -This extends the `Writer` class by adding support for upside-down and/or color -displays. A color value is an integer whose interpretation is dependent on the -display hardware and device driver. +This extends the `Writer` class by adding support for color displays. A color +value is an integer whose interpretation is dependent on the display hardware +and device driver. The Python font file uses single bit pixels. On a color +screen these are rendered using foreground and background colors. -### 2.2.1 Static method - -The following static method is added: - 1. `invert_display` Args `device`, `value=True`. The `device` is the display - instance. If `value` is set, causes text to be rendered upside down. The - `set_textpos` method should be called to ensure that text is rendered from the - bottom right hand corner (viewing the display in its normal orientation). - - If a display is to be run inverted, this method must be called prior to - instantiating a `Writer` for this display. - -### 2.2.2 Constructor +### 2.2.1 Constructor This takes the following args: 1. `device` The hardware device driver instance for the screen in use. @@ -246,20 +251,20 @@ 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. -### 2.2.3 Methods +### 2.2.2 Methods All methods of the base class are supported. Additional method: - 1. `setcolor` Args: `fgcolor=None`, `bgcolor=None`. Sets the foreground and - background colors. If one is `None` that value is left unchanged. If both - are `None` the constructor defaults are restored. Constructor defaults are - 1 and 0 for monochrome displays (`Writer`). Returns foreground - and background color values. + 1. `setcolor(fgcolor=None, bgcolor=None)`. Sets the foreground and background + colors. If one is `None` that value is left unchanged. If both are `None` the + constructor defaults are restored. Constructor defaults are 1 and 0 + for monochrome displays (`Writer`). Returns foreground and background color + values. 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`). -### 2.2.4 A performance boost +### 2.2.3 A performance boost Rendering performance of the `Cwriter` class is slow: owing to limitations in the `framebuf.blit` method the class renders glyphs one pixel at a time. There @@ -269,8 +274,8 @@ consists of a native C module. On import, `writer.py` attempts to import a module `framebuf_utils`. If this succeeds, glyph rendering will be substantially faster. If the file is not present the class will work using normal rendering. If the file exists but was -compiled for a different architecture a warning message will be printed but the -class will work using normal rendering. +compiled for a different architecture a warning message will be printed. This +is a harmless advisory - the code will run using normal rendering. The directory `framebuf_utils` contains the source file, the makefile and a version of `framebuf_utils.mpy` for `armv7m` architecture (e.g. Pyboards). @@ -280,10 +285,6 @@ to specify the `xtensawin` arch and rebuild. It is suggested that moving the appropriate `framebuf_utils.mpy` to the target is only done once the basic operation of an application has been verified. -The native module does not support the `CWriter.invert_display` option. If this -is used, the presence of the native module will have no effect. The module has -no effect on the `Writer` class which uses fast rendering by default. - The module has a `fast_mode` variable which is set `True` on import if the mode was successfully engaged. User code should treat this as read-only. diff --git a/writer/writer.py b/writer/writer.py index 834b4b4..5f09006 100644 --- a/writer/writer.py +++ b/writer/writer.py @@ -1,6 +1,9 @@ # writer.py Implements the Writer class. -# V0.35 Peter Hinch Sept 2020 Fast rendering option for color displays -# Handles colour, upside down diplays, word wrap and tab stops +# Handles colour, word wrap and tab stops + +# V0.40 Jan 2021 Improved handling of word wrap and line clip. Upside-down +# rendering no longer supported: delegate to device driver. +# V0.35 Sept 2020 Fast rendering option for color displays # Released under the MIT License (MIT). See LICENSE. # Copyright (c) 2019-2020 Peter Hinch @@ -21,6 +24,8 @@ import framebuf from uctypes import bytearray_at, addressof +__version__ = (0, 4, 0) + fast_mode = True try: try: @@ -39,7 +44,6 @@ class DisplayState(): def __init__(self): self.text_row = 0 self.text_col = 0 - self.usd = False def _get_id(device): if not isinstance(device, framebuf.FrameBuffer): @@ -60,11 +64,11 @@ class Writer(): if row is not None: if row < 0 or row >= device.height: raise ValueError('row is out of range') - s.text_row = device.height - 1 - row if s.usd else row + 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 = device.width -1 - col if s.usd else col + s.text_col = col return s.text_row, s.text_col def __init__(self, device, font, verbose=True): @@ -73,7 +77,6 @@ class Writer(): if self.devid not in Writer.state: Writer.state[self.devid] = DisplayState() self.font = font - self.usd = Writer.state[self.devid].usd # Allow to work with reverse or normal font mapping if font.hmap(): @@ -105,20 +108,12 @@ class Writer(): def _newline(self): s = self._getstate() height = self.font.height() - if self.usd: - s.text_row -= height - s.text_col = self.screenwidth - 1 - margin = s.text_row - height - y = 0 - else: - s.text_row += height - s.text_col = 0 - margin = self.screenheight - (s.text_row + height) - y = self.screenheight + margin + 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: - if self.usd: - margin = -margin self.device.scroll(0, margin) self.device.fill_rect(0, y, self.screenwidth, abs(margin), self.bgcolor) s.text_row += margin @@ -157,7 +152,6 @@ class Writer(): if pos > 0: rstr = string[pos + 1:] string = lstr - #print("[", string, "] [", lstr, "] [", rstr, "]", pos) for char in string: self._printchar(char, invert) @@ -167,14 +161,12 @@ class Writer(): def stringlen(self, string, oh=False): sc = self._getstate().text_col # Start column - #print('stringlen sc =', sc) 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: - print('sl1', string, l + sc, wd) return True # All done. Save time. char = string[-1] _, _, char_width = self.font.get_ch(char) @@ -182,7 +174,6 @@ class Writer(): l += self._truelen(char) # Last char might have blank cols on RHS else: l += char_width # Public method. Return same value as old code. - print('sl2', string, l + sc, wd, char, char_width) return l + sc > wd if oh else l # Return the printable width of a glyph less any blank columns on RHS @@ -204,7 +195,7 @@ class Writer(): break if mc + 1 == wd: break # All done: no trailing space - print('Truelen', char, wd, mc + 1) + print('Truelen', char, wd, mc + 1) # TEST return mc + 1 def _get_char(self, char, recurse): @@ -228,32 +219,18 @@ class Writer(): glyph, char_height, char_width = self.font.get_ch(char) s = self._getstate() np = None # Allow restriction on printable columns - if self.usd: - if s.text_row - char_height < 0: - if self.row_clip: + 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() - oh = s.text_col - char_width # Amount glyph would overhang edge (-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() - else: - 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 @@ -286,12 +263,6 @@ class Writer(): # Writer for colour displays or upside down rendering class CWriter(Writer): - @staticmethod - def invert_display(device, value=True): - devid = id(device) - if devid not in Writer.state: - Writer.state[devid] = DisplayState() - Writer.state[devid].usd = value def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True): super().__init__(device, font, verbose) @@ -301,9 +272,8 @@ class CWriter(Writer): self.fgcolor = fgcolor self.def_bgcolor = self.bgcolor self.def_fgcolor = self.fgcolor - fm = fast_mode and not self.usd - self._printchar = self._pchfast if fm else self._pchslow - verbose and print('Render {} using fast mode'.format('is' if fm else 'not')) + 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() @@ -333,25 +303,21 @@ class CWriter(Writer): device = self.device fgcolor = self.bgcolor if invert else self.fgcolor bgcolor = self.fgcolor if invert else self.bgcolor - usd = self.usd 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/subtract writer column - if usd: - dcol = wcol - scol - else: - dcol = wcol + scol + # 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 usd else 1 + drow += 1 if drow >= self.screenheight or drow < 0: break - s.text_col += -char_width if usd else char_width + s.text_col += char_width self.cpos += 1 def setcolor(self, fgcolor=None, bgcolor=None):