Repository navigation
Expand file tree
/
Copy pathframebuf.py
More file actions
390 lines (323 loc) · 13.7 KB
/
Copy pathframebuf.py
File metadata and controls
390 lines (323 loc) · 13.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
# SPDX-FileCopyrightText: <text> 2018 Kattni Rembor, Melissa LeBlanc-Williams
# and Tony DiCola, for Adafruit Industries.
# Copyright 2024 Brad Barnett
# Original file created by Damien P. George
# SPDX-License-Identifier: MIT
"""
`framebuf`
====================================================
*Python framebuf module, based on the micropython framebuf module.
Will not be imported on MicroPython boards since framebuf is included
in the compiled firmware.
framebuf on MicroPython does not return an Area object for methods that
write to the buffer, but BasicShapes does, so the native shape methods
here return an Area object in order to keep it consistent within this
library. It is recommended to use framebuf_plus.py instead of this if
you need to use the returned Areas so your code will be transferable.
"""
from draw import BasicShapes
from area import Area
from sys import implementation
if implementation.name == "circuitpython":
from ulab import numpy as np
else:
np = None
# Framebuf format constants:
MVLSB = 0 # Single bit displays (like SSD1306 OLED)
MONO_VLSB = 0 # Single bit displays (like SSD1306 OLED)
MONO_HLSB = 3 # Unimplemented!
MONO_HMSB = 4 # Single bit displays where the bits in a byte are horizontally mapped. Each byte occupies 8 horizontal pixels with bit 0 being the leftmost.
RGB565 = 1 # 16-bit color displays
GS2_HMSB = 5 # 2-bit color displays like the HT16K33 8x8 Matrix
GS4_HMSB = 2 # Unimplemented!
GS8 = 6 # Unimplemented!
# Font file constants:
# To maintain compatibility with MicroPython framebuf, the font should be 8x8, scale 1.
_FONT_FILE = None # Use the default font
_FONT_WIDTH = 8
_FONT_HEIGHT = 8
_FONT_SCALE = 1
class MVLSBFormat:
"""MVLSBFormat"""
depth = 1
@staticmethod
def set_pixel(framebuf, x, y, color):
"""Set a given pixel to a color."""
index = (y >> 3) * framebuf._stride + x
offset = y & 0x07
framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | (
(color != 0) << offset
)
@staticmethod
def get_pixel(framebuf, x, y):
"""Get the color of a given pixel"""
index = (y >> 3) * framebuf._stride + x
offset = y & 0x07
return (framebuf._buffer[index] >> offset) & 0x01
@staticmethod
def fill(framebuf, color):
"""completely fill/clear the buffer with a color"""
if color:
fill = 0xFF
else:
fill = 0x00
for i in range(len(framebuf._buffer)): # pylint: disable=consider-using-enumerate
framebuf._buffer[i] = fill
@staticmethod
def fill_rect(framebuf, x, y, width, height, color):
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
both the outline and interior."""
# pylint: disable=too-many-arguments
while height > 0:
index = (y >> 3) * framebuf._stride + x
offset = y & 0x07
for w_w in range(width):
framebuf._buffer[index + w_w] = (
framebuf._buffer[index + w_w] & ~(0x01 << offset)
) | ((color != 0) << offset)
y += 1
height -= 1
class MHLSBFormat:
depth = 1
@staticmethod
def set_pixel(framebuf, x, y, color):
"""Set a given pixel to a color."""
index = (y >> 3) * framebuf._stride + x
offset = 7 - (x & 0x07)
framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | (
(color != 0) << offset
)
@staticmethod
def get_pixel(framebuf, x, y):
"""Get the color of a given pixel"""
index = (x + y * framebuf._stride) >> 3
offset = 7 - (x & 0x07)
return (framebuf._buffer[index] >> offset) & 0x01
class MHMSBFormat:
"""MHMSBFormat"""
depth = 1
@staticmethod
def set_pixel(framebuf, x, y, color):
"""Set a given pixel to a color."""
index = (y * framebuf._stride + x) // 8
offset = 7 - x & 0x07
framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | (
(color != 0) << offset
)
@staticmethod
def get_pixel(framebuf, x, y):
"""Get the color of a given pixel"""
index = (y * framebuf._stride + x) // 8
offset = 7 - x & 0x07
return (framebuf._buffer[index] >> offset) & 0x01
@staticmethod
def fill(framebuf, color):
"""completely fill/clear the buffer with a color"""
if color:
fill = 0xFF
else:
fill = 0x00
for i in range(len(framebuf._buffer)): # pylint: disable=consider-using-enumerate
framebuf._buffer[i] = fill
@staticmethod
def fill_rect(framebuf, x, y, width, height, color):
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
both the outline and interior."""
# pylint: disable=too-many-arguments
for _x in range(x, x + width):
offset = 7 - _x & 0x07
for _y in range(y, y + height):
index = (_y * framebuf._stride + _x) // 8
framebuf._buffer[index] = (framebuf._buffer[index] & ~(0x01 << offset)) | (
(color != 0) << offset
)
class RGB565Format:
"""
This class implements the RGB565 format
It assumes a little-endian byte order in the frame buffer
"""
depth = 16
@staticmethod
def set_pixel(framebuf, x, y, color):
"""Set a given pixel to a color."""
index = (y * framebuf._stride + x) * 2
framebuf._buffer[index : index + 2] = (color & 0xFFFF).to_bytes(2, "little")
@staticmethod
def get_pixel(framebuf, x, y):
"""Get the color of a given pixel"""
index = (y * framebuf._stride + x) * 2
lobyte, hibyte = framebuf._buffer[index : index + 2]
r = hibyte & 0xF8
g = ((hibyte & 0x07) << 5) | ((lobyte & 0xE0) >> 5)
b = (lobyte & 0x1F) << 3
return (r << 16) | (g << 8) | b
@staticmethod
def fill(framebuf, color):
"""completely fill/clear the buffer with a color"""
rgb565_color = (color & 0xFFFF).to_bytes(2, "little")
if np:
rgb565_color_int = int.from_bytes(rgb565_color, "little")
arr = np.frombuffer(framebuf._buffer, dtype=np.uint16)
arr[:] = rgb565_color_int
else:
for i in range(0, len(framebuf._buffer), 2):
framebuf._buffer[i : i + 2] = rgb565_color
@staticmethod
def fill_rect(framebuf, x, y, width, height, color):
"""Draw a rectangle at the given location, size and color. The ``fill_rect`` method draws
both the outline and interior."""
# pylint: disable=too-many-arguments
rgb565_color = (color & 0xFFFF).to_bytes(2, "little")
if np:
rgb565_color_int = int.from_bytes(rgb565_color, "little")
arr = np.frombuffer(framebuf._buffer, dtype=np.uint16)
for _y in range(y, y + height):
arr[_y * framebuf._stride + x : _y * framebuf._stride + x + width] = (
rgb565_color_int
)
else:
for _y in range(2 * y, 2 * (y + height), 2):
offset2 = _y * framebuf._stride
for _x in range(2 * x, 2 * (x + width), 2):
index = offset2 + _x
framebuf._buffer[index : index + 2] = rgb565_color
class GS2HMSBFormat:
"""GS2HMSBFormat"""
depth = 2
@staticmethod
def set_pixel(framebuf, x, y, color):
"""Set a given pixel to a color."""
index = (y * framebuf._stride + x) >> 2
pixel = framebuf._buffer[index]
shift = (x & 0b11) << 1
mask = 0b11 << shift
color = (color & 0b11) << shift
framebuf._buffer[index] = color | (pixel & (~mask))
@staticmethod
def get_pixel(framebuf, x, y):
"""Get the color of a given pixel"""
index = (y * framebuf._stride + x) >> 2
pixel = framebuf._buffer[index]
shift = (x & 0b11) << 1
return (pixel >> shift) & 0b11
@staticmethod
def fill(framebuf, color):
"""completely fill/clear the buffer with a color"""
if color:
bits = color & 0b11
fill = (bits << 6) | (bits << 4) | (bits << 2) | (bits << 0)
else:
fill = 0x00
framebuf._buffer = [fill for i in range(len(framebuf._buffer))]
@staticmethod
def fill_rect(framebuf, x, y, width, height, color):
"""Draw the outline and interior of a rectangle at the given location, size and color."""
# pylint: disable=too-many-arguments
for _x in range(x, x + width):
for _y in range(y, y + height):
GS2HMSBFormat.set_pixel(framebuf, _x, _y, color)
class GS4HMSBFormat:
depth = 4
class GS8Format:
depth = 8
class FrameBuffer(BasicShapes):
"""FrameBuffer object.
:param buf: An object with a buffer protocol which must be large enough to contain every
pixel defined by the width, height and format of the FrameBuffer.
:param width: The width of the FrameBuffer in pixel
:param height: The height of the FrameBuffer in pixel
:param buf_format: Specifies the type of pixel used in the FrameBuffer; permissible values
are listed under Constants below. These set the number of bits used to
encode a color value and the layout of these bits in ``buf``. Where a
color value c is passed to a method, c is a small integer with an encoding
that is dependent on the format of the FrameBuffer.
:param stride: The number of pixels between each horizontal line of pixels in the
FrameBuffer. This defaults to ``width`` but may need adjustments when
implementing a FrameBuffer within another larger FrameBuffer or screen. The
``buf`` size must accommodate an increased step size.
"""
def __init__(self, buffer, width, height, format, stride=None):
# pylint: disable=too-many-arguments
self._buffer = buffer
self._width = width
self._height = height
self._stride = stride if stride is not None else width
self._font = None
if format == MONO_VLSB:
self._format = MVLSBFormat()
elif format == MONO_HLSB:
self._format = MHLSBFormat()
elif format == MONO_HMSB:
self._format = MHMSBFormat()
elif format == RGB565:
self._format = RGB565Format()
elif format == GS2_HMSB:
self._format = GS2HMSBFormat()
elif format == GS4_HMSB:
self._format = GS4HMSBFormat()
elif format == GS8:
self._format = GS8Format()
else:
raise ValueError("invalid format")
def fill_rect(self, x, y, w, h, c):
"""Draw a filled rectangle at the given location, size and color."""
self._format.fill_rect(self, x, y, w, h, c)
return Area(x, y, w, h)
def pixel(self, x, y, c=None):
"""If ``c`` is not given, get the color value of the specified pixel. If ``c`` is
given, set the specified pixel to the given color."""
if x < 0 or x >= self._width or y < 0 or y >= self._height:
return None
if c is None:
return self._format.get_pixel(self, x, y)
self._format.set_pixel(self, x, y, c)
return Area(x, y, 1, 1)
def fill(self, c):
"""Fill the entire FrameBuffer with the specified color."""
self._format.fill(self, c)
return Area(0, 0, self._width, self._height)
def scroll(self, xstep, ystep):
"""
Shift the contents of the FrameBuffer by the given vector (xstep, ystep).
This may leave a footprint of the previous colors in the FrameBuffer.
"""
# Check to make sure self._format.depth is a multiple of 8
if self._format.depth % 8 != 0:
raise ValueError(
"Scrolling is only implemented for depths that are multiples of 8"
)
BPP = self._format.depth // 8 # Bytes per pixel
# Determine the width and height of the FrameBuffer
width = self._width
height = self._height
# Calculate the number of bytes per row
bytes_per_row = width * BPP
# Iterate over each row in the appropriate order (top to bottom for ystep > 0, bottom to top for ystep < 0)
if ystep > 0:
y_range = range(height - 1, -1, -1)
else:
y_range = range(height)
# Iterate over each row in the appropriate order
for y in y_range:
# Calculate the new row index
new_y = y + ystep
# Skip rows that go out of bounds
if new_y < 0 or new_y >= height:
continue
# Calculate the byte offset for the current and new rows
offset = y * bytes_per_row
new_offset = new_y * bytes_per_row
# Iterate over each column in the appropriate order (right to left for xstep > 0, left to right for xstep < 0)
if xstep > 0:
for i in range(bytes_per_row - 1, (xstep * BPP) - 1, -1):
self._buffer[new_offset + i] = self._buffer[offset + i - (xstep * BPP)]
elif xstep < 0:
for i in range(-xstep * BPP, bytes_per_row):
self._buffer[new_offset + i] = self._buffer[offset + i + (xstep * BPP)]
else:
# If there is no x shift, copy the row as is
self._buffer[new_offset : new_offset + bytes_per_row] = self._buffer[
offset : offset + bytes_per_row
]
def FrameBuffer1(buffer, width, height, format, stride=None):
return FrameBuffer(buffer, width, height, format, stride)