OVERALL FIX

This commit is contained in:
Klemek
2024-03-13 14:02:32 +01:00
parent d5a0f87f91
commit c26ab25212
10 changed files with 0 additions and 863 deletions
View File
-325
View File
@@ -1,325 +0,0 @@
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from typing import Optional
from enum import Enum
import os.path
from explorer import Explorer
from image_view import ImageView
from input_popup import InputPopup
from file import File
from image import Image
from bitmap import Bitmap, BitmapError
class MenuEntryType(Enum):
DEFAULT = 0
NEED_FILE = 1
NEED_IMAGE = 2
SEPARATOR = 4
class App(ttk.Frame):
MENU_ENTRIES = {
"File": [
("New File", "_file_new", MenuEntryType.DEFAULT),
("Open File...", "_file_open", MenuEntryType.DEFAULT),
("", "", MenuEntryType.SEPARATOR),
("Save File", "_file_save", MenuEntryType.NEED_FILE),
("Save File As...", "_file_save_as", MenuEntryType.NEED_FILE),
("Close File", "_file_close", MenuEntryType.NEED_FILE),
("", "", MenuEntryType.SEPARATOR),
(
"New image...",
"_file_new_image",
MenuEntryType.NEED_FILE,
),
("", "", MenuEntryType.SEPARATOR),
("Quit", "_file_quit", MenuEntryType.DEFAULT),
],
"Image": [
(
"Edit Image Name...",
"_image_edit_name",
MenuEntryType.NEED_IMAGE,
),
(
"Edit Image Size...",
"_image_edit_size",
MenuEntryType.NEED_IMAGE,
), # TODO _image_edit_size
(
"Move Image Up",
"_image_move_up",
MenuEntryType.NEED_IMAGE,
),
(
"Move Image Down",
"_image_move_down",
MenuEntryType.NEED_IMAGE,
),
(
"Delete Image",
"_image_delete",
MenuEntryType.NEED_IMAGE,
),
],
"Bitmap": [
(
"Bulk .bmp Import...",
"_bmp_import_all",
MenuEntryType.NEED_FILE,
),
(
"Export All To .bmp...",
"_bmp_export_all",
MenuEntryType.NEED_FILE,
),
("", "", MenuEntryType.SEPARATOR),
(
"Import .bmp Into Image...",
"_bmp_import_image",
MenuEntryType.NEED_IMAGE,
),
("Export Image To .bmp...", "_bmp_export_image", MenuEntryType.NEED_IMAGE),
],
}
def __init__(self, parent) -> None:
super().__init__(parent)
parent.option_add("*tearOff", tk.FALSE)
parent.resizable(False, False)
self.parent = parent
self.current_file = None
self.explorer = Explorer(self, self.update)
self.explorer.grid(column=0, row=0, sticky="nsw")
self.image_view = ImageView(self)
self.image_view.grid(column=1, row=0, sticky="nsew")
self.init_menus()
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
self.open_file(None)
self.pack(fill="both", expand=True)
@property
def current_image(self) -> Optional[Image]:
if self.current_file is None:
return None
else:
return self.explorer.current_image
def update(self, force: bool = False) -> None:
self.update_title()
self.update_menus()
self.image_view.update(self.current_image)
self.explorer.update(self.current_file, force)
def update_title(self) -> None:
title = "Watchy Image Editor"
if self.current_file is not None:
title += "- "
if self.current_file.path is None:
title += "New file"
else:
title += self.current_file.filename
if self.current_file.modified:
title += "*"
self.parent.title(title)
def init_menus(self) -> None:
self.menubar = tk.Menu(self.parent)
self.parent["menu"] = self.menubar
self.menus = {}
for menu_name in self.MENU_ENTRIES:
self.menus[menu_name] = tk.Menu(self.menubar)
self.menubar.add_cascade(menu=self.menus[menu_name], label=menu_name)
for entry_name, entry_action_name, entry_type in self.MENU_ENTRIES[
menu_name
]:
if entry_type == MenuEntryType.SEPARATOR:
self.menus[menu_name].add_separator()
else:
try:
entry_action = getattr(self, entry_action_name)
except AttributeError:
entry_action = lambda: print("missing menu action")
self.menus[menu_name].add_command(
label=entry_name, command=entry_action
)
def update_menus(self) -> None:
for menu_name in self.MENU_ENTRIES:
any_enabled = False
for entry_name, entry_action, entry_type in self.MENU_ENTRIES[menu_name]:
if entry_type == MenuEntryType.NEED_FILE:
self.menus[menu_name].entryconfigure(
entry_name,
state=(
"normal" if self.current_file is not None else "disabled"
),
)
any_enabled |= self.current_file is not None
elif entry_type == MenuEntryType.NEED_IMAGE:
self.menus[menu_name].entryconfigure(
entry_name,
state=(
"normal" if self.current_image is not None else "disabled"
),
)
any_enabled |= self.current_image is not None
elif entry_type == MenuEntryType.DEFAULT:
any_enabled = True
self.menubar.entryconfigure(
menu_name, state=("normal" if any_enabled else "disabled")
)
def open_file(self, path: Optional[str], new: bool = False) -> None:
if path is None and not new:
self.current_file = None
else:
self.current_file = File(path)
self.update(force=True)
def save_file(self, path: Optional[str] = None) -> None:
if path == "":
path = filedialog.asksaveasfilename()
self.current_file.export(path)
self.open_file(path)
def _file_new(self) -> None:
self.open_file(None, True)
def _file_open(self) -> None:
path = filedialog.askopenfilename(
filetypes=File.FILE_TYPES,
defaultextension=File.FILE_TYPES,
initialfile=(
os.path.basename(self.current_file.path)
if self.current_file is not None
else None
),
initialdir=(
os.path.dirname(self.current_file.path)
if self.current_file is not None
else None
),
)
if path:
self.open_file(path)
def _file_save(self) -> None:
if self.current_file.path is None:
self._file_save_as()
else:
self.save_file(self.current_file.path)
def _file_save_as(self) -> None:
path = filedialog.asksaveasfilename(
filetypes=File.FILE_TYPES,
defaultextension=File.FILE_TYPES,
initialfile=(
os.path.basename(self.current_file.path)
if self.current_file.path is not None
else None
),
initialdir=(
os.path.dirname(self.current_file.path)
if self.current_file.path is not None
else None
),
)
if path:
self.save_file(path)
def _file_close(self) -> None:
self.open_file(None)
def _file_new_image(self) -> None:
popup = InputPopup(
self,
title="New image",
message="Please enter image name",
)
if popup.value:
self.current_file.images += [Image(popup.value, 20, 20, empty=True)]
self.update()
def _file_quit(self) -> None:
self.parent.destroy()
def _image_edit_name(self) -> None:
popup = InputPopup(
self,
title="Edit image name",
message="Please enter image name",
initial_value=self.explorer.current_image.name,
)
if popup.value:
self.explorer.current_image.name = popup.value
self.update()
def _image_move_up(self) -> None:
self.explorer.move_up()
def _image_move_down(self) -> None:
self.explorer.move_down()
def _image_delete(self) -> None:
self.explorer.delete()
def _bmp_import_all(self) -> None:
paths = filedialog.askopenfilenames(
filetypes=Bitmap.FILE_TYPES,
defaultextension=Bitmap.FILE_TYPES,
)
if paths and len(paths) > 0:
for path in paths:
name = os.path.basename(path).rstrip(".bmp")
image = self.current_file.search(name)
if image is None:
image = Image(name, 20, 20, empty=True)
self.current_file.images += [image]
try:
image.import_bmp(path)
except BitmapError as e:
pass
self.update()
def _bmp_export_all(self) -> None:
dir_path = filedialog.askdirectory()
if dir_path:
for image in self.current_file.images:
image.export_bmp(os.path.join(dir_path, f"{image.name}.bmp"))
def _bmp_import_image(self) -> None:
path = filedialog.askopenfilename(
filetypes=Bitmap.FILE_TYPES,
defaultextension=Bitmap.FILE_TYPES,
)
if path:
try:
self.current_image.import_bmp(path)
self.update()
except BitmapError as e:
messagebox.showerror(title="Bitmap import error", message=str(e))
def _bmp_export_image(self) -> None:
path = filedialog.asksaveasfilename(
filetypes=Bitmap.FILE_TYPES,
defaultextension=Bitmap.FILE_TYPES,
initialfile=f"{self.current_image.name}.bmp",
)
if path:
self.current_image.export_bmp(path)
-119
View File
@@ -1,119 +0,0 @@
from typing import Tuple
class BitmapError(Exception):
pass
class Bitmap:
HEADER_SIZE = 54
FILE_TYPES = [("Bitmap Image", "*.bmp"), ("All Files", "*.*")]
@classmethod
def write_bmp(cls, path: str, width: int, color_depth: int, data: bytes) -> None:
with open(path, mode="wb") as f:
f.write(cls.__get_bmp_data(width, color_depth, data))
@classmethod
def __get_bmp_data(cls, width: int, color_depth: int, data: bytes) -> bytes:
height = len(data) // (width * 3)
return cls.__get_header(
width, height, color_depth, len(data)
) + cls.__format_data(width, height, color_depth, data)
@classmethod
def __get_header(
cls, width: int, height: int, color_depth: int, data_len: int
) -> bytes:
header = bytes()
# BMP header
header += "BM".encode() # (0, 2) BM
header += (cls.HEADER_SIZE + data_len).to_bytes(
4, byteorder="little"
) # (2, 4) file size
header += bytes([0]) * 4 # (6, 4) application reserved
header += (cls.HEADER_SIZE).to_bytes(
4, byteorder="little"
) # (10, 4) data offset
# DIB header
header += (40).to_bytes(4, byteorder="little") # (14, 4) DIB header size
header += width.to_bytes(4, byteorder="little") # (18, 4) width
header += height.to_bytes(4, byteorder="little") # (22, 4) height
header += (1).to_bytes(2, byteorder="little") # (26, 2) color panes
header += (color_depth * 8).to_bytes(
2, byteorder="little"
) # (28, 2) bits per pixel
header += bytes([0]) * 4 # (30, 4) BI_RGB, no compression
header += (data_len).to_bytes(
4, byteorder="little"
) # (34, 4) size of raw bitmap data
header += (2835).to_bytes(
4, byteorder="little"
) # (38, 4) horizontal print resolution
header += (2835).to_bytes(
4, byteorder="little"
) # (42, 4) vertical print resolution
header += bytes([0]) * 4 # (46, 4) color in palette
header += bytes([0]) * 4 # (50, 4) 0 important colors
return header
@classmethod
def __format_data(
cls, width: int, height: int, color_depth: int, data: bytes
) -> bytes:
size = width * height * color_depth
if len(data) < size:
data += bytes([0]) * (size - len(data))
elif len(data) > size:
data = data[:size]
line_padding = (width * color_depth) % 4
output_data = bytes()
for y in range(height):
start = (height - y - 1) * color_depth * width
output_data += data[start : start + width * color_depth]
if line_padding > 0:
output_data += bytes([0]) * (4 - line_padding)
return output_data
@classmethod
def read_bmp(cls, path: str) -> Tuple[int, int, int, bytes]:
with open(path, mode="rb") as f:
bmp_data = f.read()
width, height, color_depth, data_start, data_size = cls.__read_header(bmp_data)
content_data = bmp_data[data_start:]
if data_size > 0:
content_data = content_data[:data_size]
output_data = cls.__read_formated_data(width, height, color_depth, content_data)
return width, height, color_depth, output_data
@classmethod
def __read_header(cls, bmp_data: bytes) -> Tuple[int, int, int, int, int]:
if bmp_data[0:2].decode() != "BM":
raise BitmapError("Not a Bitmap Image")
if int.from_bytes(bmp_data[30:34], byteorder="little") != 0:
raise BitmapError("Cannot read Bitmap: need no compression")
if int.from_bytes(bmp_data[26:28], byteorder="little") != 1:
raise BitmapError("Cannot read Bitmap: need 1 color panes")
width = int.from_bytes(bmp_data[18:22], byteorder="little")
height = int.from_bytes(bmp_data[22:26], byteorder="little")
color_depth = int.from_bytes(bmp_data[28:30], byteorder="little") // 8
if color_depth < 1:
raise BitmapError("Cannot read Bitmap: bits per pixels is < 8")
data_start = int.from_bytes(bmp_data[10:14], byteorder="little")
data_size = int.from_bytes(bmp_data[34:38], byteorder="little")
return width, height, color_depth, data_start, data_size
@classmethod
def __read_formated_data(
cls, width: int, height: int, color_depth: int, bmp_data: bytes
) -> bytes:
line_padding = (width * color_depth) % 4
if line_padding > 0:
real_width = width * color_depth + (4 - line_padding)
else:
real_width = width * color_depth
output_data = bytes()
for y in range(height):
start = (height - y - 1) * real_width
output_data += bmp_data[start : start + width * color_depth]
return output_data
-111
View File
@@ -1,111 +0,0 @@
from tkinter import ttk
from typing import Optional
from file import File
from image import Image
class Explorer(ttk.Frame):
def __init__(self, parent, update_callback) -> None:
super().__init__(parent)
self.current_file = None
self.current_id = None
self.update_callback = update_callback
self.explorer = ttk.Treeview(self, columns=("size"))
self.explorer.heading("#0", text="name")
self.explorer.heading("size", text="size")
self.explorer.column("#0", width=150, anchor="w")
self.explorer.column("size", width=80, anchor="w")
self.explorer.grid(row=0, column=0, sticky="nsw")
self.explorer.bind("<<TreeviewSelect>>", self.explorer_item_click)
yscrollbar = ttk.Scrollbar(self, orient="vertical", command=self.explorer.yview)
yscrollbar.grid(row=0, column=1, sticky="nsw")
self.explorer.configure(yscrollcommand=yscrollbar.set)
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
@property
def current_image(self) -> Optional[Image]:
if self.current_file is None or self.current_id is None:
return None
else:
return self.current_file.images[self.current_id]
@property
def size(self) -> int:
if self.current_file is None:
return 0
else:
return len(self.current_file.images)
def focus(self, id: int) -> None:
if self.current_file is not None and id >= 0 and id < self.size:
self.current_id = id
self.explorer.selection_set(str(id))
def move_up(self) -> None:
if self.current_id > 0:
id = self.current_id
images = self.current_file.images
images[id], images[id - 1] = images[id - 1], images[id]
self.current_id -= 1
self.focus(self.current_id)
self.update(self.current_file, False)
def move_down(self) -> None:
if self.current_id < self.size - 1:
id = self.current_id
images = self.current_file.images
images[id], images[id + 1] = images[id + 1], images[id]
self.current_id += 1
self.focus(self.current_id)
self.update(self.current_file, False)
def delete(self) -> None:
del self.current_file.images[self.current_id]
self.current_id = min(self.current_id, self.size - 1)
self.update(self.current_file, True)
def update(self, file: File, force: bool):
focus_id = self.current_id
if force or file != self.current_file:
focus_id = 0
if file is not None and file == self.current_file:
focus_id = self.current_id
ids = self.explorer.get_children()
if len(ids) > 0:
self.explorer.delete(*ids)
self.current_id = None
self.current_file = file
if self.current_file is not None:
for i, image in enumerate(self.current_file.images):
if self.explorer.exists(str(i)):
self.explorer.item(
str(i),
text=f"{image.name}{'*' if image.modified else ''}",
values=[f"{image.width}x{image.height}"],
)
else:
self.explorer.insert(
"",
"end",
iid=str(i),
text=f"{image.name}{'*' if image.modified else ''}",
values=[f"{image.width}x{image.height}"],
)
if self.size > 0 and (focus_id != self.current_id or force):
self.focus(focus_id)
def explorer_item_click(self, event) -> None:
if self.current_file is None or len(self.explorer.selection()) == 0:
self.current_id = None
else:
self.current_id = int(self.explorer.selection()[0])
self.update_callback()
-76
View File
@@ -1,76 +0,0 @@
from typing import List, Optional
import re
import os.path
from image import Image
class File:
FILE_TYPES = [("Header File", "*.h"), ("All Files", "*.*")]
def __init__(self, path: str) -> None:
self.path = path
if path is None:
self.images = []
else:
self.images = self.__read_file()
@property
def filename(self) -> str:
if self.path is None:
return None
return os.path.basename(self.path)
@property
def modified(self) -> bool:
return any(image.modified for image in self.images)
def search(self, name) -> Optional[Image]:
for image in self.images:
if image.name == name:
return image
return None
def __read_file(self) -> List[Image]:
images = []
current_image = None
with open(self.path) as f:
for line in f:
header = re.match(
r"const unsigned char (\w+) \[\] PROGMEM \= \{",
line,
)
if header:
groups = header.groups()
if current_image is None:
current_image = Image(groups[0], 0, 0)
current_image.name = groups[0]
elif current_image is not None and current_image.name is not None:
data = re.match(r"((0x\w+,? ?)+)", line.strip())
if data:
current_image.add_data(
data.groups()[0].strip().strip(",").split(", ")
)
else:
images += [current_image]
current_image.finalize()
current_image = None
comment_header = re.match(r"// '(\w+)', (\d+)x(\d+)px", line)
if comment_header:
groups = comment_header.groups()
current_image = Image(groups[0], int(groups[1]), int(groups[2]))
return images
def export(self, path: str) -> None:
with open(path, mode="w") as f:
for image in self.images:
f.write(image.export_cpp())
def __eq__(self, other: object) -> bool:
if isinstance(other, self.__class__):
return self.path == other.path
else:
return False
-94
View File
@@ -1,94 +0,0 @@
from typing import List
from math import sqrt
from bitmap import Bitmap
class Image:
def __init__(self, name: str, width: int, height: int, empty: bool = False) -> None:
self.name = name
self.width = width
self.height = height
self.modified = False
if empty:
self.data = [0] * ((width * height) // 8)
self.modified = True
else:
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
def add_data(self, raw_data: List[str]) -> None:
for v in raw_data:
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:
position = self.__get_position(x, y)
chunk_id = position // 8
try:
return self.data[chunk_id] & (1 << (7 - position % 8)) > 0
except:
return False
def set_pixel(self, x: int, y: int, v: bool) -> None:
position = self.__get_position(x, y)
chunk_id = position // 8
if v != self.get_pixel(x, y):
try:
if v:
self.data[chunk_id] |= 1 << (7 - position % 8)
else:
self.data[chunk_id] &= ~(1 << (7 - position % 8))
except:
pass
self.modified = True
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
def export_bmp(self, path: str) -> None:
Bitmap.write_bmp(path, self.width, 3, self.__get_color_bytes())
def __set_color_bytes(self, color_depth: int, data: bytes) -> None:
for y in range(self.height):
for x in range(self.width):
position = (y * self.width + x) * color_depth
colors = data[position : position + color_depth]
mean_color = sum(c for c in colors) / color_depth
if mean_color < 128:
self.set_pixel(x, y, True)
def import_bmp(self, path: str) -> None:
self.width, self.height, color_depth, bmp_data = Bitmap.read_bmp(path)
self.data = [0] * ((self.width * self.height) // 8)
self.__set_color_bytes(color_depth, bmp_data)
def export_cpp(self) -> str:
# 16 per line
output = [
f"// '{self.name}', {self.width}x{self.height}px",
f"const unsigned char {self.name} [] PROGMEM = {{",
]
while len(self.data) > 16:
output += ["\t" + ", ".join(f"0x{v:02x}" for v in self.data[0:16]) + ","]
self.data = self.data[16:]
output += ["\t" + ", ".join(f"0x{v:02x}" for v in self.data), "};", ""]
return "\n".join(output)
-103
View File
@@ -1,103 +0,0 @@
import tkinter as tk
from tkinter import ttk
from image import Image
class ImageView(ttk.Frame):
INITIAL_DRAW_SCALE = 3
def __init__(self, parent) -> None:
super().__init__(parent, height=650, width=650)
self.draw_scale = self.INITIAL_DRAW_SCALE
self.current_image = None
self.canvas = tk.Canvas(self, width=0, height=0, background="white")
self.canvas.place(in_=self, anchor="c", relx=0.5, rely=0.5)
self.canvas.bind("<Button-1>", self.click_canvas_b1)
self.canvas.bind("<B1-Motion>", self.click_canvas_b1)
self.canvas.bind(
"<ButtonRelease-1>", lambda event: self.update(self.current_image)
)
self.canvas.bind("<Button-3>", self.click_canvas_b3)
self.canvas.bind("<B3-Motion>", self.click_canvas_b3)
self.canvas.bind(
"<ButtonRelease-3>", lambda event: self.update(self.current_image)
)
self.canvas.bind("<MouseWheel>", self.zoom_canvas)
self.canvas.bind("<Button-4>", self.zoom_canvas_up)
self.canvas.bind("<Button-5>", self.zoom_canvas_down)
self.bind("<MouseWheel>", self.zoom_canvas)
self.bind("<Button-4>", self.zoom_canvas_up)
self.bind("<Button-5>", self.zoom_canvas_down)
def update(self, image: Image) -> None:
if self.current_image != image:
self.draw_scale = self.INITIAL_DRAW_SCALE
if image is None:
self.canvas.configure(
width=0,
height=0,
background="white",
)
else:
try:
self.canvas.configure(
width=(image.width * self.draw_scale),
height=(image.height * self.draw_scale),
background="white",
)
self.canvas.delete("all")
for x in range(image.width):
for y in range(image.height):
if image.get_pixel(x, y):
self.canvas.create_rectangle(
x * self.draw_scale + 1,
y * self.draw_scale + 1,
(x + 1) * self.draw_scale + 1,
(y + 1) * self.draw_scale + 1,
fill="black",
outline="",
)
except tk.TclError:
pass
self.current_image = image
def click_canvas_b1(self, event):
self.click_canvas(True, event)
def click_canvas_b3(self, event):
self.click_canvas(False, event)
def click_canvas(self, value: bool, event):
if self.current_image is None:
return
x = int(event.x / self.draw_scale)
y = int(event.y / self.draw_scale)
self.current_image.set_pixel(x, y, value)
self.canvas.create_rectangle(
x * self.draw_scale + 1,
y * self.draw_scale + 1,
(x + 1) * self.draw_scale + 1,
(y + 1) * self.draw_scale + 1,
fill=("black" if value else "white"),
outline="",
)
def zoom_canvas(self, event):
if event.delta > 0:
self.zoom_canvas_up()
else:
self.zoom_canvas_down()
def zoom_canvas_up(self, event=None):
self.draw_scale *= 2
self.update(self.current_image)
def zoom_canvas_down(self, event=None):
self.draw_scale /= 2
self.update(self.current_image)
-26
View File
@@ -1,26 +0,0 @@
import tkinter as tk
from tkinter import ttk
class InputPopup(tk.Toplevel):
def __init__(self, parent, *, title: str, message: str, initial_value: str = ""):
super().__init__(parent)
self.title(title)
self.value = None
label = ttk.Label(self, text=message)
label.pack()
self.entry = ttk.Entry(self)
self.entry.insert(0, initial_value)
self.entry.pack()
button = ttk.Button(self, text="Ok", command=self.cleanup)
button.pack()
parent.wait_window(self)
def cleanup(self):
self.value = self.entry.get()
self.destroy()
-9
View File
@@ -1,9 +0,0 @@
import tkinter as tk
import os.path
from app import App
if __name__ == "__main__":
app = App(tk.Tk())
app.mainloop()
Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB