Skip to content

Commit f79459e

Browse files
committed
writer.py: Disallow glyph clipping.
1 parent e365a10 commit f79459e

File tree

1 file changed

+42
-45
lines changed

1 file changed

+42
-45
lines changed

writer/writer.py

Lines changed: 42 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# writer.py Implements the Writer class.
22
# Handles colour, word wrap and tab stops
33

4+
# V0.5.2 May 2025 Fix bug whereby glyph clipping might be attempted.
45
# V0.5.1 Dec 2022 Support 4-bit color display drivers.
56
# V0.5.0 Sep 2021 Color now requires firmware >= 1.17.
67
# V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682).
@@ -23,24 +24,24 @@
2324

2425
import framebuf
2526
from uctypes import bytearray_at, addressof
26-
from sys import implementation
2727

28-
__version__ = (0, 5, 1)
28+
__version__ = (0, 5, 2)
2929

30-
fast_mode = True # Does nothing. Kept to avoid breaking code.
3130

32-
class DisplayState():
31+
class DisplayState:
3332
def __init__(self):
3433
self.text_row = 0
3534
self.text_col = 0
3635

36+
3737
def _get_id(device):
3838
if not isinstance(device, framebuf.FrameBuffer):
39-
raise ValueError('Device must be derived from FrameBuffer.')
39+
raise ValueError("Device must be derived from FrameBuffer.")
4040
return id(device)
4141

42+
4243
# Basic Writer class for monochrome displays
43-
class Writer():
44+
class Writer:
4445

4546
state = {} # Holds a display state for each device
4647

@@ -52,13 +53,13 @@ def set_textpos(device, row=None, col=None):
5253
s = Writer.state[devid] # Current state
5354
if row is not None:
5455
if row < 0 or row >= device.height:
55-
raise ValueError('row is out of range')
56+
raise ValueError("row is out of range")
5657
s.text_row = row
5758
if col is not None:
5859
if col < 0 or col >= device.width:
59-
raise ValueError('col is out of range')
60+
raise ValueError("col is out of range")
6061
s.text_col = col
61-
return s.text_row, s.text_col
62+
return s.text_row, s.text_col
6263

6364
def __init__(self, device, font, verbose=True):
6465
self.devid = _get_id(device)
@@ -67,16 +68,20 @@ def __init__(self, device, font, verbose=True):
6768
Writer.state[self.devid] = DisplayState()
6869
self.font = font
6970
if font.height() >= device.height or font.max_width() >= device.width:
70-
raise ValueError('Font too large for screen')
71+
raise ValueError("Font too large for screen")
7172
# Allow to work with reverse or normal font mapping
7273
if font.hmap():
7374
self.map = framebuf.MONO_HMSB if font.reverse() else framebuf.MONO_HLSB
7475
else:
75-
raise ValueError('Font must be horizontally mapped.')
76+
raise ValueError("Font must be horizontally mapped.")
7677
if verbose:
77-
fstr = 'Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}.'
78+
fstr = "Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}."
7879
print(fstr.format(font.reverse(), device.width, device.height))
79-
print('Start row = {} col = {}'.format(self._getstate().text_row, self._getstate().text_col))
80+
print(
81+
"Start row = {} col = {}".format(
82+
self._getstate().text_row, self._getstate().text_col
83+
)
84+
)
8085
self.screenwidth = device.width # In pixels
8186
self.screenheight = device.height
8287
self.bgcolor = 0 # Monochrome background and foreground colors
@@ -90,7 +95,6 @@ def __init__(self, device, font, verbose=True):
9095
self.glyph = None # Current char
9196
self.char_height = 0
9297
self.char_width = 0
93-
self.clip_width = 0
9498

9599
def _getstate(self):
96100
return Writer.state[self.devid]
@@ -123,30 +127,30 @@ def height(self): # Property for consistency with device
123127

124128
def printstring(self, string, invert=False):
125129
# word wrapping. Assumes words separated by single space.
126-
q = string.split('\n')
130+
q = string.split("\n")
127131
last = len(q) - 1
128132
for n, s in enumerate(q):
129133
if s:
130134
self._printline(s, invert)
131135
if n != last:
132-
self._printchar('\n')
136+
self._printchar("\n")
133137

134138
def _printline(self, string, invert):
135139
rstr = None
136140
if self.wrap and self.stringlen(string, True): # Length > self.screenwidth
137141
pos = 0
138142
lstr = string[:]
139143
while self.stringlen(lstr, True): # Length > self.screenwidth
140-
pos = lstr.rfind(' ')
144+
pos = lstr.rfind(" ")
141145
lstr = lstr[:pos].rstrip()
142146
if pos > 0:
143-
rstr = string[pos + 1:]
147+
rstr = string[pos + 1 :]
144148
string = lstr
145-
149+
146150
for char in string:
147151
self._printchar(char, invert)
148152
if rstr is not None:
149-
self._printchar('\n')
153+
self._printchar("\n")
150154
self._printline(rstr, invert) # Recurse
151155

152156
def stringlen(self, string, oh=False):
@@ -176,7 +180,7 @@ def _truelen(self, char):
176180
mc = 0 # Max non-blank column
177181
data = glyph[(wd - 1) // 8] # Last byte of row 0
178182
for row in range(ht): # Glyph row
179-
for col in range(wd -1, -1, -1): # Glyph column
183+
for col in range(wd - 1, -1, -1): # Glyph column
180184
gbyte, gbit = divmod(col, 8)
181185
if gbit == 0: # Next glyph byte
182186
data = glyph[row * gbytes + gbyte]
@@ -187,47 +191,42 @@ def _truelen(self, char):
187191
break
188192
if mc + 1 == wd:
189193
break # All done: no trailing space
190-
# print('Truelen', char, wd, mc + 1) # TEST
194+
# print('Truelen', char, wd, mc + 1) # TEST
191195
return mc + 1
192196

193197
def _get_char(self, char, recurse):
194198
if not recurse: # Handle tabs
195-
if char == '\n':
199+
if char == "\n":
196200
self.cpos = 0
197-
elif char == '\t':
201+
elif char == "\t":
198202
nspaces = self.tab - (self.cpos % self.tab)
199203
if nspaces == 0:
200204
nspaces = self.tab
201205
while nspaces:
202206
nspaces -= 1
203-
self._printchar(' ', recurse=True)
207+
self._printchar(" ", recurse=True)
204208
self.glyph = None # All done
205209
return
206210

207211
self.glyph = None # Assume all done
208-
if char == '\n':
212+
if char == "\n":
209213
self._newline()
210214
return
211215
glyph, char_height, char_width = self.font.get_ch(char)
212216
s = self._getstate()
213-
np = None # Allow restriction on printable columns
214217
if s.text_row + char_height > self.screenheight:
215218
if self.row_clip:
216219
return
217220
self._newline()
218-
oh = s.text_col + char_width - self.screenwidth # Overhang (+ve)
219-
if oh > 0:
221+
if s.text_col + char_width - self.screenwidth > 0: # Glyph would overhang
220222
if self.col_clip or self.wrap:
221-
np = char_width - oh # No. of printable columns
222-
if np <= 0:
223-
return
223+
return # Can't clip a glyph: discard
224224
else:
225225
self._newline()
226226
self.glyph = glyph
227227
self.char_height = char_height
228228
self.char_width = char_width
229-
self.clip_width = char_width if np is None else np
230-
229+
231230
# Method using blitting. Efficient rendering for monochrome displays.
232231
# Tested on SSD1306. Invert is for black-on-white rendering.
233232
def _printchar(self, char, invert=False, recurse=False):
@@ -238,8 +237,8 @@ def _printchar(self, char, invert=False, recurse=False):
238237
buf = bytearray(self.glyph)
239238
if invert:
240239
for i, v in enumerate(buf):
241-
buf[i] = 0xFF & ~ v
242-
fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map)
240+
buf[i] = 0xFF & ~v
241+
fbc = framebuf.FrameBuffer(buf, self.char_width, self.char_height, self.map)
243242
self.device.blit(fbc, s.text_col, s.text_row)
244243
s.text_col += self.char_width
245244
self.cpos += 1
@@ -252,26 +251,24 @@ def tabsize(self, value=None):
252251
def setcolor(self, *_):
253252
return self.fgcolor, self.bgcolor
254253

254+
255255
# Writer for colour displays.
256256
class CWriter(Writer):
257-
258257
@staticmethod
259258
def create_color(ssd, idx, r, g, b):
260259
c = ssd.rgb(r, g, b)
261-
if not hasattr(ssd, 'lut'):
260+
if not hasattr(ssd, "lut"):
262261
return c
263262
if not 0 <= idx <= 15:
264-
raise ValueError('Color nos must be 0..15')
263+
raise ValueError("Color nos must be 0..15")
265264
x = idx << 1
266-
ssd.lut[x] = c & 0xff
265+
ssd.lut[x] = c & 0xFF
267266
ssd.lut[x + 1] = c >> 8
268267
return idx
269268

270269
def __init__(self, device, font, fgcolor=None, bgcolor=None, verbose=True):
271-
if not hasattr(device, 'palette'):
272-
raise OSError('Incompatible device driver.')
273-
if implementation[1] < (1, 17, 0):
274-
raise OSError('Firmware must be >= 1.17.')
270+
if not hasattr(device, "palette"):
271+
raise OSError("Incompatible device driver.")
275272

276273
super().__init__(device, font, verbose)
277274
if bgcolor is not None: # Assume monochrome.
@@ -287,7 +284,7 @@ def _printchar(self, char, invert=False, recurse=False):
287284
if self.glyph is None:
288285
return # All done
289286
buf = bytearray_at(addressof(self.glyph), len(self.glyph))
290-
fbc = framebuf.FrameBuffer(buf, self.clip_width, self.char_height, self.map)
287+
fbc = framebuf.FrameBuffer(buf, self.char_width, self.char_height, self.map)
291288
palette = self.device.palette
292289
palette.bg(self.fgcolor if invert else self.bgcolor)
293290
palette.fg(self.bgcolor if invert else self.fgcolor)

0 commit comments

Comments
 (0)