Skip to content

Commit

Permalink
Fast text rendering (#50)
Browse files Browse the repository at this point in the history
This adds:
- sub-shape clipping at the level of character rendering
- fast viper-based lookup of font-to-py font data
- bitmap and metric caching for font-to-py fonts

The end result is a 10x speedup in text rendering.

Fixes #46.
  • Loading branch information
corranwebster authored Nov 6, 2024
1 parent 7c117e4 commit f5ff274
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 20 deletions.
50 changes: 46 additions & 4 deletions src/tempe/font.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@


import framebuf
from uctypes import bytearray_at, addressof

from .util import bisect16

class AbstractFont:
def measure(self, text):
Expand All @@ -26,15 +28,35 @@ def __init__(self, mod):
self.height = mod.height()
self.baseline = mod.baseline()
self.monospaced = mod.monospaced()
self._mvfont = memoryview(mod._mvfont)
self._msvp = memoryview(mod._mvsp)
self._cache = {}

def measure(self, text):
width = 0
for char in text:
width += self.font.get_ch(char)[2]
n = ord(char)
if n not in self._cache:
self._cache[n] = self._get_char(n)
width += self._cache[n][2]
return (0, self.height - self.baseline, width, self.height)

def bitmap(self, char):
return self.font.get_ch(char)
n = ord(char)
if n not in self._cache:
self._cache[n] = self._get_char(n)
return self._cache[n]

def clear_cache(self):
self._cache = {}

def _get_char(self, n):
offset = bisect16(self._msvp, n, len(self._msvp) >> 2) << 3
width = self._mvfont[offset] | (self._mvfont[offset + 1] << 8)
next_offset = offset + 2 + (((width - 1) >> 3) + 1) << 4
buf = self._mvfont[offset + 2:next_offset]
buf = bytearray_at(addressof(buf), len(buf))
return buf, self.height, width


class MicroFont(BitmapFont):
Expand Down Expand Up @@ -62,12 +84,32 @@ def __init__(self, mod):
self.height = mod.height
self.baseline = mod.baseline
self.monospaced = mod.monospaced
self._mvfont = memoryview(mod._mvfont)
self._msvp = memoryview(mod._mvsp)
self._cache = {}

def measure(self, text):
width = 0
for char in text:
width += self.font.get_ch(char)[2]
n = ord(char)
if n not in self._cache:
self._cache[n] = self._get_char(n)
width += self._cache[n][2]
return (0, self.height - self.baseline, width, self.height)

def bitmap(self, char):
return self.font.get_ch(char)
n = ord(char)
if n not in self._cache:
self._cache[n] = self._get_char(n)
return self._cache[n]

def clear_cache(self):
self._cache = {}

def _get_char(self, n):
offset = bisect16(self._msvp, n, len(self._msvp) >> 2) << 3
width = self._mvfont[offset] | (self._mvfont[offset + 1] << 8)
next_offset = offset + 2 + (((width - 1) >> 3) + 1) << 4
buf = self._mvfont[offset + 2:next_offset]
buf = bytearray_at(addressof(buf), len(buf))
return buf, self.height, width
13 changes: 11 additions & 2 deletions src/tempe/surface.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,13 +151,22 @@ def markers(self, layer, geometry, colors, markers, clip=None):
self.add_shape(layer, points)
return points

def text(self, layer, geometry, colors, texts, font=None, clip=None):
def text(
self,
layer,
geometry,
colors,
texts,
font=None,
line_spacing=0,
clip=None,
):
from .text import Text

geometry = self._check_geometry(geometry, 2)
colors = self._check_colors(colors)
texts = self._check_texts(texts)
text = Text(geometry, colors, texts, font=font, clip=clip)
text = Text(geometry, colors, texts, font=font, line_spacing=line_spacing, clip=clip)
self.add_shape(layer, text)
return text

Expand Down
3 changes: 3 additions & 0 deletions src/tempe/surface.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -334,6 +334,7 @@ class Surface:
colors: Iterable[int] | int,
text: Iterable[str] | str,
font: AbstractFont | None = None,
line_spacing: int = 0,
clip: tuple[int, int, int, int] | None = None,
) -> Text:
"""Create a new Text object and add it to the layer.
Expand All @@ -351,6 +352,8 @@ class Surface:
font : AbstractFont | None
The font to use for the text. If None, the default FrameBuffer
8x8 monospaced font will be used.
line_spacing: int
Add more or less space between adjacent lines.
clip : tuple[int, int, int, int] | None
A clipping rectangle for the text.
"""
Expand Down
58 changes: 44 additions & 14 deletions src/tempe/text.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,44 +11,74 @@

class Text(ColoredGeometry):
def __init__(
self, geometry, colors, texts, *, bold=False, font=None, surface=None, clip=None
self,
geometry,
colors,
texts,
*,
bold=False,
font=None,
line_spacing=0,
surface=None,
clip=None,
):
super().__init__(geometry, colors, surface=surface, clip=clip)
self.texts = texts
self.bold = bold
self.font = font
self.line_spacing = line_spacing

def __iter__(self):
yield from zip(self.geometry, self.colors, self.texts)

def draw(self, buffer, x=0, y=0):
def draw_raster(self, raster):
buffer = raster.fbuf
x = raster.x
y = raster.y
w = raster.w
h = raster.h
if self.font is None:
line_height = 10
line_height = 10 + self.line_spacing
for geometry, color, text in self:
px = geometry[0] - x
py = geometry[1] - y
if px > w or py > h:
continue
for i, line in enumerate(text.splitlines()):
line_y = py + i * line_height
if line_y > h:
break
if px + 8 * len(line) < 0 or line_y + line_height < 0:
continue
buffer.text(line, px, py + i * line_height, color)
if self.bold:
buffer.text(line, px + 1, py + i * line_height, color)
elif isinstance(self.font, BitmapFont):
line_height = self.font.height
line_height = self.font.height + self.line_spacing
palette_buf = array("H", [BLIT_KEY_RGB565, 0xFFFF])
palette = framebuf.FrameBuffer(palette_buf, 2, 1, framebuf.RGB565)
for geometry, color, text in self:
palette_buf[1] = color
py = geometry[1] - y
for i, line in enumerate(text.splitlines()):
px = geometry[0] - x
for char in line:
buf, height, width = self.font.bitmap(char)
buf = bytearray(buf)
fbuf = framebuf.FrameBuffer(
buf, width, height, framebuf.MONO_HLSB
)
buffer.blit(fbuf, px, py, BLIT_KEY_RGB565, palette)
px += width
if py > h:
continue
for line in text.splitlines():
if (px := geometry[0] - x) > w:
break
if py + line_height > 0:
for char in line:
buf, height, width = self.font.bitmap(char)
if px + width >= 0:
fbuf = framebuf.FrameBuffer(
buf, width, height, framebuf.MONO_HLSB
)
buffer.blit(fbuf, px, py, BLIT_KEY_RGB565, palette)
px += width
if px > w:
break
py += line_height
if py > h:
break

def update(self, geometry=None, colors=None, texts=None):
if texts is not None:
Expand Down
1 change: 1 addition & 0 deletions src/tempe/text.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class Text(ColoredGeometry[tuple[int, int]]):
*,
bold: bool = False,
font: AbstractFont | None = None,
letter_spacing: int = 0,
surface: "tempe.surface.Surface | None" = None,
clip: rectangle | None = None,
): ...
Expand Down

0 comments on commit f5ff274

Please sign in to comment.