import traceback from collections import namedtuple from collections.abc import Callable from tkinter import * import tkinter.messagebox as messagebox import tkinter.filedialog as filedialog from tkinter.ttk import * from functools import wraps from pathlib import Path from platform import system from PIL import ImageOps from PIL.ImagePalette import ImagePalette from PIL.ImageTk import Image, PhotoImage from serial import Serial Resolution = namedtuple("Resolution", ("height", "width")) 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")) IWII_PALETTE = ImagePalette( "P", [ 255, 255, 255, #white 0, 255, 255, #cyan 255, 0, 255, #magenta 255, 255, 0, #yellow 0, 0, 0, #black ] ) def widget_action(func: Callable[[Event], None]) -> Callable[[Event], None]: # decorator to wrap widget actions in an error handler that displays exceptions in error message windows @wraps(func) def wrapper(event: Event): try: func(event) except NotImplementedError as e: messagebox.showwarning( title="Warning", message=f"{func.__name__} not implemented", parent=event.widget.master ) except SystemExit: # probably unnecessary, but we don't want to accidentally intercept requests to exit raise except Exception as e: messagebox.showerror( title="Error", message=str(e), detail=traceback.format_exc(), parent=event.widget.master ) quit_photobooth(event) exit(1) return wrapper @widget_action def open_preview(event: Event) -> None: file = filedialog.askopenfilename( parent=event.widget.master, title="Open Image", initialdir=Path.home(), filetypes=[ ("Supported Image Files", " ".join(SUPPORTED_IMAGE_TYPES)) ] ) if not file: return elif Path(file).exists() and Path(file).is_file(): # load image data and resize to fit the IWII resolution preserving aspect ratio image_data = ImageOps.contain(Image.open(file), IWII_RESOLUTION) p_img = Image.new('P', image_data.size) p_img.putpalette(IWII_PALETTE) conv = image_data.quantize(palette=p_img).convert("RGB") photo = PhotoImage(conv, size=IWII_RESOLUTION) event.widget.create_image(0,0, anchor=CENTER, 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 = conv @widget_action def send_to_printer(event: Event) -> None: if not event.widget.master.photobooth_image: messagebox.showwarning( title="Warning", message="Nothing to print. Please take a picture first.", parent=event.widget.master ) else: with Serial( "/dev/ttyUSB0", baudrate=9600, xonxoff=True, rtscts=False ) as printer: printer.write(event.widget.master.photobooth_image.tobytes()) @widget_action def clear_preview(event: Event) -> None: if event.widget.master.photobooth_image: # remove the image data event.widget.master.photobooth_image = None # clear the canvas event.widget.delete("all") # remove the reference to the data formerly displayed on the canvas so it gets GC'd del event.widget._displayed_photo @widget_action def quit_photobooth(event: Event) -> None: event.widget.master.destroy() event.widget.master.quit() def photobooth(): app = Tk() app.photobooth_image = None app.title("PhotoBooth") style = Style() match system(): case "Windows": try: style.theme_use("winnative") except TclError: style.theme_use("vista") case "Darwin": style.theme_use("aqua") case "Linux": 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 _: style.theme_use("default") preview = Canvas(master=app, height=IWII_RESOLUTION.height, width=IWII_RESOLUTION.width) preview.bind("", open_preview) preview.bind("", send_to_printer) preview.bind("c", clear_preview) preview.bind("q", quit_photobooth) preview.focus_set() preview.pack(side="top") app.mainloop() if __name__ == "__main__": photobooth()