use PIL for image handling

This commit is contained in:
Brennen Raimer
2025-03-18 16:42:52 -04:00
parent 12af0cee8d
commit 7e84886bb3

View File

@@ -1,33 +1,40 @@
import mimetypes
import traceback import traceback
from collections import namedtuple
from collections.abc import Callable from collections.abc import Callable
import tkinter as tk from tkinter import *
import tkinter.messagebox as messagebox import tkinter.messagebox as messagebox
import tkinter.filedialog as filedialog import tkinter.filedialog as filedialog
import tkinter.ttk as ttk from tkinter.ttk import *
from functools import wraps from functools import wraps
from pathlib import Path from pathlib import Path
from platform import system from platform import system
from PIL import ImageOps
from PIL.ImageTk import Image, PhotoImage
from serial import Serial from serial import Serial
if not mimetypes.inited: Resolution = namedtuple("Resolution", ("height", "width"))
mimetypes.init() IWII_RESOLUTION = Resolution(576, 720)
SUPPORTED_IMAGE_TYPES = sorted("*"+ext for ext, type_ in Image.registered_extensions().items() if type_ in ("PNG", "GIF", "JPEG", "WEBP", "BMP"))
def handle_action_exceptions(func: Callable): def widget_action(func: Callable):
# decorator to wrap widget actions in an error handler that displays exceptions in error message windows
@wraps(func) @wraps(func)
def wrapper(event: tk.Event): def wrapper(event: Event):
try: try:
func(event) func(event)
except NotImplementedError as e: except NotImplementedError as e:
messagebox.showwarning( messagebox.showwarning(
title="Warning", title="Warning",
message=f"{func.__name__} Not Implemented", message=f"{func.__name__} not implemented",
parent=event.widget.master parent=event.widget.master
) )
except SystemExit:
# probably unnecessary, but we don't want to accidentally intercept requests to exit
raise
except Exception as e: except Exception as e:
messagebox.showerror( messagebox.showerror(
title="Error", title="Error",
@@ -41,51 +48,66 @@ def handle_action_exceptions(func: Callable):
return wrapper return wrapper
@handle_action_exceptions @widget_action
def open_preview(event: tk.Event): def open_preview(event: Event):
file = filedialog.askopenfilename( file = filedialog.askopenfilename(
parent=event.widget.master, parent=event.widget.master,
title="Open Image", title="Open Image",
initialdir=Path.home(), initialdir=Path.home(),
filetypes=[ filetypes=[
(mimetype, "*"+ext) for ext, mimetype in mimetypes.types_map.items() ("Supported Image Files", " ".join(SUPPORTED_IMAGE_TYPES))
if ext in (".png",".gif", ".pgm", ".ppm")
] ]
) )
if Path(file).exists(): if not file:
# don't GC the photo when this function exits return
global _photo elif Path(file).exists() and Path(file).is_file():
_photo=tk.PhotoImage(file=file) # load image data and resize to fit the IWII resolution preserving aspect ratio
event.widget.create_image(0,0, anchor=tk.NW, image=_photo) image_data = ImageOps.contain(Image.open(file), IWII_RESOLUTION)
photo = PhotoImage(image_data, size=IWII_RESOLUTION)
event.widget.create_image(0,0, anchor=NW, image=photo)
# tkinter does not maintain a reference to the local variables of this function upon return
# without references, the photo and its data get garbage collected and nothing is display
# therefore we must create references to them to keep them around
event.widget._displayed_photo=photo
event.widget.master.photobooth_image = image_data
@handle_action_exceptions @widget_action
def send_to_iwii(event: tk.Event): def send_to_iwii(event: Event):
raise NotImplementedError raise NotImplementedError
def photobooth(): def photobooth():
app = tk.Tk() app = Tk()
app.photobooth_image = None
app.title("PhotoBooth") app.title("PhotoBooth")
style = ttk.Style()
style = Style()
match system(): match system():
case "Windows": case "Windows":
try: try:
style.theme_use("winnative") style.theme_use("winnative")
except tk.TclError: except TclError:
style.theme_use("vista") style.theme_use("vista")
case "Darwin": case "Darwin":
style.theme_use("aqua") style.theme_use("aqua")
case "Linux": case "Linux":
style.theme_use("alt") style.theme_use("alt")
# https://python-forum.io/thread-31235.html
try:
# this is so stupid! you have to intentionally do this just to initialize the
# required namespace to set variables to hide hidden files by default
app.tk.call("tk_getOpenFile", "-intentionalnonsense")
except TclError:
app.setvar("::tk::dialog::file::showHiddenBtn", 1)
app.setvar("::tk::dialog::file::showHiddenVar", 0)
case _: case _:
style.theme_use("default") style.theme_use("default")
preview = tk.Canvas(master=app, height=576, width=720) preview = Canvas(master=app, height=IWII_RESOLUTION.height, width=IWII_RESOLUTION.width)
preview.bind("<space>", open_preview) preview.bind("<space>", open_preview)
preview.bind("<return>", send_to_iwii) preview.bind("<Return>", send_to_iwii)
preview.focus_set() preview.focus_set()
preview.pack(side="top") preview.pack(side="top")
app.mainloop() app.mainloop()