export to bmp and small fixes

This commit is contained in:
klemek
2021-08-25 15:10:59 +02:00
parent 6e07cabe37
commit ab3f582f82
+134 -25
View File
@@ -3,30 +3,103 @@ from tkinter import ttk
from tkinter import filedialog from tkinter import filedialog
from typing import List, Optional, Tuple from typing import List, Optional, Tuple
import re import re
from math import sqrt, log2, ceil
DRAW_SCALE = 3 DRAW_SCALE = 3
class Bitmap:
HEADER_SIZE = 54
FILE_TYPES = [("Bitmap Image", "*.bmp"), ("All Files", "*.*")]
@classmethod
def get_bmp_data(cls, width: int, data: bytes) -> bytes:
height = len(data) // (width * 3)
return cls.__get_header(width, height, len(data)) + cls.__format_data(
width, height, data
)
@classmethod
def __get_header(cls, width: int, height: int, data_len: int) -> bytes:
header = bytes()
# BMP header
header += "BM".encode() # (2) BM
header += (cls.HEADER_SIZE + data_len).to_bytes(
4, byteorder="little"
) # (4) file size
header += bytes([0]) * 4 # (4) application reserved
header += (cls.HEADER_SIZE).to_bytes(4, byteorder="little") # (4) data offset
# DIB header
header += (40).to_bytes(4, byteorder="little") # (4) DIB header size
header += width.to_bytes(4, byteorder="little") # (4) width
header += height.to_bytes(4, byteorder="little") # (4) height
header += (1).to_bytes(2, byteorder="little") # (2) color panes
header += (24).to_bytes(2, byteorder="little") # (2) bits per pixel
header += bytes([0]) * 4 # (4) BI_RGB, no compression
header += (data_len).to_bytes(
4, byteorder="little"
) # (4) size of raw bitmap data
header += (2835).to_bytes(
4, byteorder="little"
) # (4) horizontal print resolution
header += (2835).to_bytes(
4, byteorder="little"
) # (4) vertical print resolution
header += bytes([0]) * 4 # (4) color in palette
header += bytes([0]) * 4 # (4) 0 important colors
return header
@classmethod
def __format_data(cls, width: int, height: int, data: bytes) -> bytes:
size = width * height * 3
if len(data) < size:
data += bytes([0]) * (size - len(data))
elif len(data) > size:
data = data[:size]
line_padding = (width * 3) % 4
if line_padding == 0:
return data
output_data = bytes()
for y in range(height):
start = y * 3 * width
output_data += data[start : start + width]
output_data += bytes([0]) * line_padding
return output_data
class Image: class Image:
def __init__(self, comment_name: str, width: int, height: int) -> None: def __init__(self, name: str, width: int, height: int) -> None:
self.comment_name = comment_name self.name = name
self.name = None
self.width = width self.width = width
self.height = height self.height = height
self.data = [] self.data = []
def finalize(self) -> None:
if self.width == 0:
pixels = len(self.data) * 8
width = int(sqrt(pixels))
while width > 1 and pixels % width != 0:
width -= 1
self.width = width
self.height = pixels // width
print(f"image '{self.name}': {self.width}x{self.height}")
def add_data(self, raw_data: List[str]) -> None: def add_data(self, raw_data: List[str]) -> None:
for v in raw_data: for v in raw_data:
self.data += [int(v, 16)] self.data += [int(v, 16)]
def get_position(self, x: int, y: int) -> int:
real_width = (len(self.data) * 8) // self.height
return y * real_width + x
def get_pixel(self, x: int, y: int) -> bool: def get_pixel(self, x: int, y: int) -> bool:
position = y * self.width + x position = self.get_position(x, y)
chunk_id = position // 8 chunk_id = position // 8
return self.data[chunk_id] & (1 << (7 - position % 8)) > 0 return self.data[chunk_id] & (1 << (7 - position % 8)) > 0
def set_pixel(self, x: int, y: int, v: bool) -> None: def set_pixel(self, x: int, y: int, v: bool) -> None:
position = y * self.width + x position = self.get_position(x, y)
chunk_id = position // 8 chunk_id = position // 8
byte = pow(2, 7 - position % 8) byte = pow(2, 7 - position % 8)
if v: if v:
@@ -34,8 +107,20 @@ class Image:
else: else:
self.data[chunk_id] &= ~(1 << (7 - position % 8)) self.data[chunk_id] &= ~(1 << (7 - position % 8))
def get_color_bytes(self) -> bytes:
output = bytes()
for y in range(self.height):
for x in range(self.width):
if self.get_pixel(x, y):
output += bytes([0, 0, 0])
else:
output += bytes([255, 255, 255])
return output
class File: class File:
FILE_TYPES = [("Header File", "*.h"), ("All Files", "*.*")]
def __init__(self, path: str) -> None: def __init__(self, path: str) -> None:
self.path = path self.path = path
if path is None: if path is None:
@@ -50,23 +135,25 @@ class File:
with open(self.path) as f: with open(self.path) as f:
for line in f: for line in f:
if current_image is not None: header = re.match(
header = re.match( r"const unsigned char (\w+) \[\] PROGMEM \= \{",
r"const unsigned char (\w+) \[\] PROGMEM \= \{", line,
line, )
) if header:
if header: groups = header.groups()
groups = header.groups() if current_image is None:
current_image.name = groups[0] current_image = Image(groups[0], 0, 0)
elif current_image.name is not None: current_image.name = groups[0]
data = re.match(r"((0x\w+,? ?)+)", line.strip()) elif current_image is not None and current_image.name is not None:
if data: data = re.match(r"((0x\w+,? ?)+)", line.strip())
current_image.add_data( if data:
data.groups()[0].strip().strip(",").split(", ") current_image.add_data(
) data.groups()[0].strip().strip(",").split(", ")
else: )
images += [current_image] else:
current_image = None images += [current_image]
current_image.finalize()
current_image = None
comment_header = re.match(r"// '(\w+)', (\d+)x(\d+)px", line) comment_header = re.match(r"// '(\w+)', (\d+)x(\d+)px", line)
if comment_header: if comment_header:
groups = comment_header.groups() groups = comment_header.groups()
@@ -111,7 +198,12 @@ class App(ttk.Frame):
menu_file.add_command(label="New", command=lambda: self.open_file("")) menu_file.add_command(label="New", command=lambda: self.open_file(""))
menu_file.add_command( menu_file.add_command(
label="Open...", label="Open...",
command=lambda: self.open_file(filedialog.askopenfilename()), command=lambda: self.open_file(
filedialog.askopenfilename(
filetypes=File.FILE_TYPES,
defaultextension=File.FILE_TYPES,
)
),
) )
menu_file.add_command( menu_file.add_command(
label="Save", label="Save",
@@ -119,7 +211,11 @@ class App(ttk.Frame):
) )
menu_file.add_command( menu_file.add_command(
label="Save As...", label="Save As...",
command=lambda: self.save_file(filedialog.asksaveasfilename()), command=lambda: self.save_file(
filedialog.asksaveasfilename(
filetypes=File.FILE_TYPES, defaultextension=File.FILE_TYPES
)
),
) )
menu_file.add_command( menu_file.add_command(
label="Close", label="Close",
@@ -327,7 +423,20 @@ class App(ttk.Frame):
pass # TODO pass # TODO
def export_bmp(self) -> None: def export_bmp(self) -> None:
pass # TODO if self.current_image is None:
return
path = filedialog.asksaveasfilename(
filetypes=Bitmap.FILE_TYPES,
defaultextension=Bitmap.FILE_TYPES,
initialfile=f"{self.current_image.name}.bmp",
)
if path is not None:
with open(path, mode="wb") as f:
f.write(
Bitmap.get_bmp_data(
self.current_image.width, self.current_image.get_color_bytes()
)
)
if __name__ == "__main__": if __name__ == "__main__":