1
1
# writer.py Implements the Writer class.
2
2
# Handles colour, word wrap and tab stops
3
3
4
+ # V0.5.2 May 2025 Fix bug whereby glyph clipping might be attempted.
4
5
# V0.5.1 Dec 2022 Support 4-bit color display drivers.
5
6
# V0.5.0 Sep 2021 Color now requires firmware >= 1.17.
6
7
# V0.4.3 Aug 2021 Support for fast blit to color displays (PR7682).
23
24
24
25
import framebuf
25
26
from uctypes import bytearray_at , addressof
26
- from sys import implementation
27
27
28
- __version__ = (0 , 5 , 1 )
28
+ __version__ = (0 , 5 , 2 )
29
29
30
- fast_mode = True # Does nothing. Kept to avoid breaking code.
31
30
32
- class DisplayState () :
31
+ class DisplayState :
33
32
def __init__ (self ):
34
33
self .text_row = 0
35
34
self .text_col = 0
36
35
36
+
37
37
def _get_id (device ):
38
38
if not isinstance (device , framebuf .FrameBuffer ):
39
- raise ValueError (' Device must be derived from FrameBuffer.' )
39
+ raise ValueError (" Device must be derived from FrameBuffer." )
40
40
return id (device )
41
41
42
+
42
43
# Basic Writer class for monochrome displays
43
- class Writer () :
44
+ class Writer :
44
45
45
46
state = {} # Holds a display state for each device
46
47
@@ -52,13 +53,13 @@ def set_textpos(device, row=None, col=None):
52
53
s = Writer .state [devid ] # Current state
53
54
if row is not None :
54
55
if row < 0 or row >= device .height :
55
- raise ValueError (' row is out of range' )
56
+ raise ValueError (" row is out of range" )
56
57
s .text_row = row
57
58
if col is not None :
58
59
if col < 0 or col >= device .width :
59
- raise ValueError (' col is out of range' )
60
+ raise ValueError (" col is out of range" )
60
61
s .text_col = col
61
- return s .text_row , s .text_col
62
+ return s .text_row , s .text_col
62
63
63
64
def __init__ (self , device , font , verbose = True ):
64
65
self .devid = _get_id (device )
@@ -67,16 +68,20 @@ def __init__(self, device, font, verbose=True):
67
68
Writer .state [self .devid ] = DisplayState ()
68
69
self .font = font
69
70
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" )
71
72
# Allow to work with reverse or normal font mapping
72
73
if font .hmap ():
73
74
self .map = framebuf .MONO_HMSB if font .reverse () else framebuf .MONO_HLSB
74
75
else :
75
- raise ValueError (' Font must be horizontally mapped.' )
76
+ raise ValueError (" Font must be horizontally mapped." )
76
77
if verbose :
77
- fstr = ' Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}.'
78
+ fstr = " Orientation: Horizontal. Reversal: {}. Width: {}. Height: {}."
78
79
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
+ )
80
85
self .screenwidth = device .width # In pixels
81
86
self .screenheight = device .height
82
87
self .bgcolor = 0 # Monochrome background and foreground colors
@@ -90,7 +95,6 @@ def __init__(self, device, font, verbose=True):
90
95
self .glyph = None # Current char
91
96
self .char_height = 0
92
97
self .char_width = 0
93
- self .clip_width = 0
94
98
95
99
def _getstate (self ):
96
100
return Writer .state [self .devid ]
@@ -123,30 +127,30 @@ def height(self): # Property for consistency with device
123
127
124
128
def printstring (self , string , invert = False ):
125
129
# word wrapping. Assumes words separated by single space.
126
- q = string .split (' \n ' )
130
+ q = string .split (" \n " )
127
131
last = len (q ) - 1
128
132
for n , s in enumerate (q ):
129
133
if s :
130
134
self ._printline (s , invert )
131
135
if n != last :
132
- self ._printchar (' \n ' )
136
+ self ._printchar (" \n " )
133
137
134
138
def _printline (self , string , invert ):
135
139
rstr = None
136
140
if self .wrap and self .stringlen (string , True ): # Length > self.screenwidth
137
141
pos = 0
138
142
lstr = string [:]
139
143
while self .stringlen (lstr , True ): # Length > self.screenwidth
140
- pos = lstr .rfind (' ' )
144
+ pos = lstr .rfind (" " )
141
145
lstr = lstr [:pos ].rstrip ()
142
146
if pos > 0 :
143
- rstr = string [pos + 1 :]
147
+ rstr = string [pos + 1 :]
144
148
string = lstr
145
-
149
+
146
150
for char in string :
147
151
self ._printchar (char , invert )
148
152
if rstr is not None :
149
- self ._printchar (' \n ' )
153
+ self ._printchar (" \n " )
150
154
self ._printline (rstr , invert ) # Recurse
151
155
152
156
def stringlen (self , string , oh = False ):
@@ -176,7 +180,7 @@ def _truelen(self, char):
176
180
mc = 0 # Max non-blank column
177
181
data = glyph [(wd - 1 ) // 8 ] # Last byte of row 0
178
182
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
180
184
gbyte , gbit = divmod (col , 8 )
181
185
if gbit == 0 : # Next glyph byte
182
186
data = glyph [row * gbytes + gbyte ]
@@ -187,47 +191,42 @@ def _truelen(self, char):
187
191
break
188
192
if mc + 1 == wd :
189
193
break # All done: no trailing space
190
- # print('Truelen', char, wd, mc + 1) # TEST
194
+ # print('Truelen', char, wd, mc + 1) # TEST
191
195
return mc + 1
192
196
193
197
def _get_char (self , char , recurse ):
194
198
if not recurse : # Handle tabs
195
- if char == ' \n ' :
199
+ if char == " \n " :
196
200
self .cpos = 0
197
- elif char == ' \t ' :
201
+ elif char == " \t " :
198
202
nspaces = self .tab - (self .cpos % self .tab )
199
203
if nspaces == 0 :
200
204
nspaces = self .tab
201
205
while nspaces :
202
206
nspaces -= 1
203
- self ._printchar (' ' , recurse = True )
207
+ self ._printchar (" " , recurse = True )
204
208
self .glyph = None # All done
205
209
return
206
210
207
211
self .glyph = None # Assume all done
208
- if char == ' \n ' :
212
+ if char == " \n " :
209
213
self ._newline ()
210
214
return
211
215
glyph , char_height , char_width = self .font .get_ch (char )
212
216
s = self ._getstate ()
213
- np = None # Allow restriction on printable columns
214
217
if s .text_row + char_height > self .screenheight :
215
218
if self .row_clip :
216
219
return
217
220
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
220
222
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
224
224
else :
225
225
self ._newline ()
226
226
self .glyph = glyph
227
227
self .char_height = char_height
228
228
self .char_width = char_width
229
- self .clip_width = char_width if np is None else np
230
-
229
+
231
230
# Method using blitting. Efficient rendering for monochrome displays.
232
231
# Tested on SSD1306. Invert is for black-on-white rendering.
233
232
def _printchar (self , char , invert = False , recurse = False ):
@@ -238,8 +237,8 @@ def _printchar(self, char, invert=False, recurse=False):
238
237
buf = bytearray (self .glyph )
239
238
if invert :
240
239
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 )
243
242
self .device .blit (fbc , s .text_col , s .text_row )
244
243
s .text_col += self .char_width
245
244
self .cpos += 1
@@ -252,26 +251,24 @@ def tabsize(self, value=None):
252
251
def setcolor (self , * _ ):
253
252
return self .fgcolor , self .bgcolor
254
253
254
+
255
255
# Writer for colour displays.
256
256
class CWriter (Writer ):
257
-
258
257
@staticmethod
259
258
def create_color (ssd , idx , r , g , b ):
260
259
c = ssd .rgb (r , g , b )
261
- if not hasattr (ssd , ' lut' ):
260
+ if not hasattr (ssd , " lut" ):
262
261
return c
263
262
if not 0 <= idx <= 15 :
264
- raise ValueError (' Color nos must be 0..15' )
263
+ raise ValueError (" Color nos must be 0..15" )
265
264
x = idx << 1
266
- ssd .lut [x ] = c & 0xff
265
+ ssd .lut [x ] = c & 0xFF
267
266
ssd .lut [x + 1 ] = c >> 8
268
267
return idx
269
268
270
269
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." )
275
272
276
273
super ().__init__ (device , font , verbose )
277
274
if bgcolor is not None : # Assume monochrome.
@@ -287,7 +284,7 @@ def _printchar(self, char, invert=False, recurse=False):
287
284
if self .glyph is None :
288
285
return # All done
289
286
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 )
291
288
palette = self .device .palette
292
289
palette .bg (self .fgcolor if invert else self .bgcolor )
293
290
palette .fg (self .bgcolor if invert else self .fgcolor )
0 commit comments