diff --git a/DRIVERS.md b/DRIVERS.md index 335c37d..3a99cc4 100644 --- a/DRIVERS.md +++ b/DRIVERS.md @@ -18,9 +18,9 @@ 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') +wri_serif.draw_text('Tuesday\n') +wri_sans.draw_text('8 Nov 2016\n') +wri_sans.draw_text('10.30am') display.show() # Display the result ``` @@ -33,10 +33,10 @@ display and a complete example of an SSD1306 driver may be found The ``Writer`` class exposes the following class methods: - 1. ``set_textpos`` Args: ``row``, ``col``. This determines where on screen any + 1. ``set_position`` Args: ``x``, ``y``. 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 + corner. Arguments are in pixels with positive values representing right and + down 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 @@ -49,7 +49,7 @@ of characters is maintained regardless of the font in use. ## Method - 1. ``printstring`` Arg: a text string. Outputs a text string at the current + 1. ``draw_text`` Arg: a text string. Outputs a text string at the current insertion point. Newline characters are honoured. ## Note on the Writer class @@ -110,7 +110,7 @@ buffer/device. ### Specifying the font file -Each font file has a ``get_ch()`` function accepting an ASCII character as its +Each font file has a ``get_char()`` 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 @@ -171,7 +171,7 @@ b'\x1b\x01\x35\x01\x4f\x01\x75\x01\x9e\x01\xb2\x01\xcc\x01\xe0\x01'\ _mvfont = memoryview(_font) # Boilerplate code omitted -def get_ch(ch): +def get_char(ch): # validate ch, if out of range use '?' # get offsets into _font and retrieve char width # Return: memoryview of bitmap, height and width @@ -187,7 +187,7 @@ 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 +``get_char()`` returns a memoryview of an individual glyph with its dimensions and contains all the bytes required to render the character including trailing space. diff --git a/FONT_TO_PY.md b/FONT_TO_PY.md index 6fdecd8..8d7689a 100644 --- a/FONT_TO_PY.md +++ b/FONT_TO_PY.md @@ -114,7 +114,7 @@ gc.collect() micropython.mem_info() def foo(): - addr, height, width = freeserif.get_ch('a') + addr, height, width = freeserif.get_char('a') foo() diff --git a/README.md b/README.md index e78724f..1ab17ec 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ all pixels of a character are rendered 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 +hand-designed binary font files: these are optimised for rendering at a specific size. # Font file interface @@ -79,13 +79,11 @@ provided to font-to-py: ``hmap`` Returns ``True`` if font is horizontally mapped. Should return ``True`` ``reverse`` Returns ``True`` if bit reversal was specified. Should return ``False`` ``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. -Glyphs are returned with the ``get_ch`` method. Its argument is a character +Glyphs are returned with the ``get_char`` method. Its argument is a character and it returns the following values: - * A ``memoryview`` object containg the glyph bytes. + * A ``memoryview`` object containing the glyph bytes. * The height in pixels. * The character width in pixels. diff --git a/driver_test.py b/driver_test.py index 04a6ec4..34c33b6 100644 --- a/driver_test.py +++ b/driver_test.py @@ -71,9 +71,9 @@ else: # 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') +serif.draw_text('Tuesday\n') +sans.draw_text('8 Nov 2016\n') +sans.draw_text('10.30am') display.show() diff --git a/font_test.py b/font_test.py index 645fcf8..ef96d0f 100644 --- a/font_test.py +++ b/font_test.py @@ -143,7 +143,7 @@ def test_font(fontfile, string): height = myfont.height() for row in range(height): for char in string: - data, _, width = myfont.get_ch(char) + data, _, width = myfont.get_char(char) if myfont.hmap(): render_row_hmap(data, row, height, width, myfont.reverse()) else: @@ -167,7 +167,7 @@ def test_file(fontfile, height, string, *, minchar=32, maxchar=126, defchar=ord( height = myfont.height() for row in range(height): for char in string: - data, _, width = myfont.get_ch(char) + data, _, width = myfont.get_char(char) if myfont.hmap(): render_row_hmap(data, row, height, width, myfont.reverse()) else: diff --git a/writer.py b/writer.py index 2a03c82..c1219ba 100644 --- a/writer.py +++ b/writer.py @@ -29,71 +29,128 @@ 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 + # these attributes and set_position are common to all Writer instances + x_pos = 0 + y_pos = 0 + device = None + screen_height = 0 + screen_width = 0 + draw_pixel = None @classmethod - def set_textpos(cls, row, col): - cls.text_row = row - cls.text_col = col + def set_position(cls, x, y): + cls.x_pos = x + cls.y_pos = y - @classmethod - def set_clip(cls, row_clip, col_clip): - cls.row_clip = row_clip - cls.col_clip = col_clip - - def __init__(self, device, font): + def __init__(self, device, font, rotation=None): super().__init__() self.device = device + self.set_font(font) + self.set_rotation(rotation) + + def set_font(self, font): self.font = font + self._draw_char = self._draw_vmap_char if font.hmap(): - raise OSError('Font must be vertically mapped') - self.screenwidth = device.width # In pixels - self.screenheight = device.height + self._draw_char = self._draw_hmap_char + + @classmethod + def set_rotation(cls, rotation=None): + rotation = 0 if not rotation else rotation % 360 + if not rotation: + cls.draw_pixel = cls._draw_pixel + elif rotation == 90: + cls.draw_pixel = cls._draw_pixel_90 + elif rotation == 180: + cls.draw_pixel = cls._draw_pixel_180 + elif rotation == 270: + cls.draw_pixel = cls._draw_pixel_270 + else: + raise ValueError('rotation must be falsy or one of 90, 180 or 270') + + if not rotation or rotation == 180: + cls.screen_width = cls.device.width + cls.screen_height = cls.device.height + else: + cls.screen_width = cls.device.height + cls.screen_height = cls.device.width + + def _draw_pixel(self, x, y, color): + self.device.pixel(x, y, color) + + def _draw_pixel_90(self, x, y, color): + self.device.pixel(self.device.width - y, x, color) + + def _draw_pixel_180(self, x, y, color): + self.device.pixel(self.device.width - x, self.device.height - y, color) + + def _draw_pixel_270(self, x, y, color): + self.device.pixel(y, self.device.height - x, color) 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 + Writer.x_pos = 0 + Writer.y_pos += self.font.height() - def printstring(self, string): + def draw_text(self, string): for char in string: - self._printchar(char) + self._draw_char(char) - def _printchar(self, char): + def _draw_hmap_char(self, char): if char == '\n': self._newline() return - glyph, char_height, char_width = self.font.get_ch(char) - if Writer.text_row + char_height > self.screenheight: - if Writer.row_clip: - return + + glyph, char_height, char_width = self.font.get_char(char) + + if Writer.x_pos + char_width > self.screen_width: + self._newline() + + div, mod = divmod(char_width, 8) + bytes_per_row = div + 1 if mod else div + + for glyph_row_i in range(char_height): + glyph_row_start = glyph_row_i * bytes_per_row + glyph_row = int.from_bytes( + glyph[glyph_row_start:glyph_row_start + bytes_per_row], + 'little' + ) + if not glyph_row: + continue + x = Writer.x_pos + y = Writer.y_pos + glyph_row_i + for glyph_col_i in range(char_width): + if glyph_row & (1 << glyph_col_i): + self.draw_pixel(x, y, 1) + x += 1 + + Writer.x_pos += char_width + + def _draw_vmap_char(self, char): + if char == '\n': + self._newline() + return + + glyph, char_height, char_width = self.font.get_char(char) + + if Writer.x_pos + char_width > self.screen_width: self._newline() - 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 - device = self.device - for scol in range(char_width): # Source column - dcol = scol + Writer.text_col # Destination column - drow = Writer.text_row # Destination row - for srow in range(char_height): # Source row - gbyte, gbit = divmod(srow, 8) - if drow >= self.screenheight: - break - if gbit == 0: # Next glyph byte - data = glyph[scol * gbytes + gbyte] - device.pixel(dcol, drow, data & (1 << gbit)) - drow += 1 - Writer.text_col += char_width + bytes_per_col = div + 1 if mod else div + + for glyph_col_i in range(char_width): + glyph_col_start = glyph_col_i * bytes_per_col + glyph_col = int.from_bytes( + glyph[glyph_col_start:glyph_col_start + bytes_per_col], + 'little' + ) + if not glyph_col: + continue + x = Writer.x_pos + glyph_col_i + y = Writer.y_pos + for glyph_row_i in range(char_height): + if glyph_col & (1 << glyph_row_i): + self.draw_pixel(x, y, 1) + y += 1 + + Writer.x_pos += char_width