-
Notifications
You must be signed in to change notification settings - Fork 9
/
Copy pathadafruit_bitmapsaver.py
197 lines (158 loc) · 6.53 KB
/
adafruit_bitmapsaver.py
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
# SPDX-FileCopyrightText: 2019 Dave Astels for Adafruit Industries
# SPDX-FileCopyrightText: 2022 Matt Land
#
# SPDX-License-Identifier: MIT
"""
`adafruit_bitmapsaver`
================================================================================
Save a displayio.Bitmap (and associated displayio.Palette) in a BMP file.
Make a screenshot (the contents of a displayio.Display) and save in a BMP file.
* Author(s): Dave Astels, Matt Land
Implementation Notes
--------------------
**Hardware:**
**Software and Dependencies:**
* Adafruit CircuitPython firmware for the supported boards:
https://github.com/adafruit/circuitpython/releases
"""
# imports
import gc
import struct
import board
from displayio import Bitmap, Palette, Display, ColorConverter
try:
from typing import Tuple, Optional, Union
from io import BufferedWriter
except ImportError:
pass
__version__ = "0.0.0+auto.0"
__repo__ = "https://github.com/adafruit/Adafruit_CircuitPython_BitmapSaver.git"
def _write_bmp_header(output_file: BufferedWriter, filesize: int) -> None:
output_file.write(bytes("BM", "ascii"))
output_file.write(struct.pack("<I", filesize))
output_file.write(b"\00\x00")
output_file.write(b"\00\x00")
output_file.write(struct.pack("<I", 54))
def _write_dib_header(output_file: BufferedWriter, width: int, height: int) -> None:
output_file.write(struct.pack("<I", 40))
output_file.write(struct.pack("<I", width))
output_file.write(struct.pack("<I", height))
output_file.write(struct.pack("<H", 1))
output_file.write(struct.pack("<H", 24))
for _ in range(24):
output_file.write(b"\x00")
def _bytes_per_row(source_width: int) -> int:
pixel_bytes = 3 * source_width
padding_bytes = (4 - (pixel_bytes % 4)) % 4
return pixel_bytes + padding_bytes
def _rotated_height_and_width(pixel_source: Union[Bitmap, Display]) -> Tuple[int, int]:
# flip axis if the display is rotated
if isinstance(pixel_source, Display) and (pixel_source.rotation % 180 != 0):
return pixel_source.height, pixel_source.width
return pixel_source.width, pixel_source.height
def _rgb565_to_bgr_tuple(color: int) -> Tuple[int, int, int]:
blue = (color << 3) & 0x00F8 # extract each of the RGB triple into it's own byte
green = (color >> 3) & 0x00FC
red = (color >> 8) & 0x00F8
return blue, green, red
def rgb565_to_rgb888(rgb565):
"""
Convert from an integer representing rgb565 color into an integer
representing rgb888 color.
:param rgb565: Color to convert
:return int: rgb888 color value
"""
# Shift the red value to the right by 11 bits.
red5 = rgb565 >> 11
# Shift the green value to the right by 5 bits and extract the lower 6 bits.
green6 = (rgb565 >> 5) & 0b111111
# Extract the lower 5 bits for blue.
blue5 = rgb565 & 0b11111
# Convert 5-bit red to 8-bit red.
red8 = round(red5 / 31 * 255)
# Convert 6-bit green to 8-bit green.
green8 = round(green6 / 63 * 255)
# Convert 5-bit blue to 8-bit blue.
blue8 = round(blue5 / 31 * 255)
# Combine the RGB888 values into a single integer
rgb888_value = (red8 << 16) | (green8 << 8) | blue8
return rgb888_value
# pylint:disable=too-many-locals
def _write_pixels(
output_file: BufferedWriter,
pixel_source: Union[Bitmap, Display],
palette: Optional[Union[Palette, ColorConverter]],
) -> None:
saving_bitmap = isinstance(pixel_source, Bitmap)
width, height = _rotated_height_and_width(pixel_source)
row_buffer = bytearray(_bytes_per_row(width))
result_buffer = False
for y in range(height, 0, -1):
buffer_index = 0
if saving_bitmap:
# pixel_source: Bitmap
for x in range(width):
pixel = pixel_source[x, y - 1]
if isinstance(palette, Palette):
color = palette[pixel] # handled by save_pixel's guardians
elif isinstance(palette, ColorConverter):
converted = palette.convert(pixel)
converted_888 = rgb565_to_rgb888(converted)
color = converted_888
for _ in range(3):
row_buffer[buffer_index] = color & 0xFF
color >>= 8
buffer_index += 1
else:
# pixel_source: Display
result_buffer = bytearray(2048)
data = pixel_source.fill_row(y - 1, result_buffer)
for i in range(width):
pixel565 = (data[i * 2] << 8) + data[i * 2 + 1]
for b in _rgb565_to_bgr_tuple(pixel565):
row_buffer[buffer_index] = b & 0xFF
buffer_index += 1
output_file.write(row_buffer)
if result_buffer:
for i in range(width * 2):
result_buffer[i] = 0
gc.collect()
# pylint:enable=too-many-locals
def save_pixels(
file_or_filename: Union[str, BufferedWriter],
pixel_source: Union[Display, Bitmap] = None,
palette: Optional[Union[Palette, ColorConverter]] = None,
) -> None:
"""Save pixels to a 24 bit per pixel BMP file.
If pixel_source if a displayio.Bitmap, save it's pixels through palette.
If it's a displayio.Display, a palette isn't required.
:param file_or_filename: either the file to save to, or it's absolute name
:param pixel_source: the Bitmap or Display to save
:param palette: the Palette to use for looking up colors in the bitmap
"""
if not pixel_source:
if not getattr(board, "DISPLAY", None):
raise ValueError("Second argument must be a Bitmap or Display")
pixel_source = board.DISPLAY
if isinstance(pixel_source, Bitmap):
if not isinstance(palette, Palette) and not isinstance(palette, ColorConverter):
raise ValueError(
"Third argument must be a Palette or ColorConverter for a Bitmap save"
)
elif not isinstance(pixel_source, Display):
raise ValueError("Second argument must be a Bitmap or Display")
try:
if isinstance(file_or_filename, str):
output_file = open( # pylint: disable=consider-using-with
file_or_filename, "wb"
)
else:
output_file = file_or_filename
width, height = _rotated_height_and_width(pixel_source)
filesize = 54 + height * _bytes_per_row(width)
_write_bmp_header(output_file, filesize)
_write_dib_header(output_file, width, height)
_write_pixels(output_file, pixel_source, palette)
except Exception as ex:
raise ex
output_file.close()