diff --git a/connector.py b/connector.py new file mode 100644 index 0000000..6cee322 --- /dev/null +++ b/connector.py @@ -0,0 +1,115 @@ +import tkinter as tk +import socket +import threading + +PORT = 12345 + +USAGE="""USAGE: To connect to existing server input IP-Address on the right +USAGE: To start a server click on \"Listen\" Button""" + +class Connector(tk.Frame): + def __init__(self, master): + print(USAGE) + super().__init__(master) + + self.master = master + self.successful = False + self.in_mainloop = True + self.frm_connect = tk.Frame(self) + self.frm_connect.columnconfigure(1, minsize=100) + self.btn_connect = tk.Button(self.frm_connect, text="Connect", command=lambda: self.start(self.connect)) + self.ent_ip = tk.Entry(self.frm_connect) + self.btn_connect.grid(row=0, column=0, padx=5, pady=2.5) + self.ent_ip.grid(row=0, column=1, padx=5) + + self.rowconfigure([0, 1], minsize=30) + self.frm_connect.grid(row=0, columnspan=2) + self.btn_listen = tk.Button(self, text="Listen", command=lambda: self.start(self.listen)) + self.btn_listen.grid(row=1, column=0, padx=5, pady=2.5, sticky="w") + self.btn_cancel = tk.Button(self, text="Cancel", command=self.stop) + self.btn_cancel.grid(row=1, column=1, sticky="w") + self.btn_cancel.grid_remove() + + self.thread = threading.Thread(target=lambda: "dummy") + + def enable_buttons(self): + self.btn_connect["state"] = "active" + self.btn_listen["state"] = "active" + + def disable_buttons(self): + self.btn_connect["state"] = "disable" + self.btn_listen["state"] = "disable" + + def start(self, target): + self.disable_buttons() + self.btn_cancel.grid() + self.thread = threading.Thread(target=target) + self.thread.start() + + def stop(self): + if self.in_mainloop: + self.enable_buttons() + self.btn_cancel.grid_remove() + self.sock.close() + + def listen(self): + print(f"STATUS: Listening on port {PORT}") + self.sock = socket.socket() + try: + self.sock.bind(("", PORT)) + self.sock.listen(1) + self.conn, addr = self.sock.accept() + except OSError as e: + print(e) + self.stop() + return + print(f"STATUS: Got connection from {addr[0]}") + self.successful = True + self.role = "server" + self.master.quit() + + def connect(self): + self.sock = socket.socket() + ip = self.ent_ip.get() + #[DEBUG] + if not ip: ip = "localhost" + print(f"STATUS: Connecting to {ip}") + try: self.sock.connect((ip, PORT)) + except OSError as e: + self.stop() + print(e) + return + print("STATUS: Connection successful") + self.successful = True + self.conn = self.sock + self.role = "client" + self.master.quit() + + def cleanup(self): + self.in_mainloop = False + if self.thread.is_alive(): self.sock.close() + + def is_successful(self): + return self.successful + + def get_results(self): + return self.conn, self.role + +if __name__ == "__main__": + connector = Connector() + connector.mainloop() + if connector.successful: + connection = connector.conn + role = connector.role + connector.destroy() + #WTF window has to be destroyed before communication otherwise very laggy + if role == "server": + connection.send(b"Hello from the server side!") + print(connection.recv(50).decode()) + elif role == "client": + print(connection.recv(50).decode()) + connection.send(b"Hello from the client side!") + input("close...") + connection.close() + elif connector.thread.is_alive(): connector.sock.close() + \ No newline at end of file diff --git a/editor.py b/editor.py new file mode 100644 index 0000000..1d5e69f --- /dev/null +++ b/editor.py @@ -0,0 +1,86 @@ +import tkinter as tk +from PIL import ImageTk, Image, ImageFilter, ImageDraw + +USAGE = """USAGE: Mousewheel Up = Blur+ +USAGE: Mousewheel Down = Blur- +USAGE: Mousebutton Left = Create Rectangle (Drag) +USAGE: Mousebutton Right = Delete Rectangle (Click) +USAGE: Enter = Confirm and Exchange""" + +class Editor(tk.Canvas): + def __init__(self, master, filename, **kwargs): + print(USAGE) + self.successful = False + self.master = master + self.rects = {} + self.current_rect = -1 + self.blur = 0.0 + self.image = Image.open(filename) + self.blurred = self.image.copy() + self.image_gui = ImageTk.PhotoImage(self.blurred) + kwargs["width"] = self.image_gui.width() + kwargs["height"] = self.image_gui.height() + super().__init__(master, **kwargs) + self.image_id = self.create_image(0, 0, image=self.image_gui, anchor="nw") + self.bind("", self.change_blur) + self.bind("", self.new_rect) + self.bind("", self.resize_rect) + self.bind("", self.create_rect) + self.bind("", self.finish) + self.focus_force() + + def change_blur(self, event): + self.blur += 0.5 * (event.delta / 120) + self.blur = max(0.0, self.blur) + tmp = self.blurred + self.blurred = self.image.filter(ImageFilter.GaussianBlur(self.blur)) + tmp.close() + self.image_gui = ImageTk.PhotoImage(self.blurred) + self.itemconfigure(self.image_id, image=self.image_gui) + + def new_rect(self, event): + coords = [event.x, event.y, event.x, event.y] + self.current_rect = self.create_rectangle(*coords) + self.rects[self.current_rect] = coords + + + def resize_rect(self, event): + self.rects[self.current_rect][2] = event.x + self.rects[self.current_rect][3] = event.y + self.coords(self.current_rect, *self.rects[self.current_rect]) + + def create_rect(self, _): + def make_deleter(handle): + return lambda _: self.delete_rect(handle) + self.itemconfigure(self.current_rect, fill="BLACK") + self.tag_bind(self.current_rect, "", make_deleter(self.current_rect)) + + def delete_rect(self, rect): + self.delete(rect) + del self.rects[rect] + + def finish(self, _): + print("STATUS: Image locked and sent") + self.successful = True + self.master.quit() + + def cleanup(self): + self.image.close() + self.blurred.close() + + def is_successful(self): + return self.successful + + def get_results(self): + imgs_with_rects = [self.blurred, self.image.copy()] + for img in imgs_with_rects: + for rect in self.rects.values(): + draw = ImageDraw.Draw(img) + draw.rectangle(rect, "BLACK", "BLACK") + return imgs_with_rects[0], imgs_with_rects[1], self.image + +if __name__ == "__main__": + window = tk.Tk() + editor = Editor(window, "test.png") + editor.pack() + window.mainloop() \ No newline at end of file diff --git a/protocol.py b/protocol.py new file mode 100644 index 0000000..d8a9fe8 --- /dev/null +++ b/protocol.py @@ -0,0 +1,66 @@ +from os import system +import tkinter as tk +from tkinter.filedialog import askopenfilename, asksaveasfilename +from connector import Connector +from editor import Editor +from verifier import Verifier + +FILETYPES = [("Image Files", (".png", ".jpg", ".jpeg"))] + +def main(): + window = tk.Tk() + connector = Connector(window) + connector.pack() + window.mainloop() + if not connector.is_successful(): + # triggered only when exiting forcefully + connector.cleanup() + return + conn, role = connector.get_results() + connector.destroy() + + while True: + filename = askopenfilename(parent=window, filetypes=FILETYPES) + if not filename: + conn.close() + return + editor = Editor(window, filename) + editor.pack() + window.mainloop() + if not editor.is_successful(): + conn.close() + editor.cleanup() + return + images = editor.get_results() + editor.destroy() + + verifier = Verifier(window, conn, role, images) + if verifier.has_paniced(): + verifier.cleanup() + return + verifier.pack() + window.mainloop() + if not verifier.is_successful(): + verifier.cleanup() + return + image = verifier.get_results() + + filename = asksaveasfilename(parent=window, filetypes=[("Image Files", ".png")]) + if filename: + if not ".png" in filename: filename += ".png" + image.save(filename) + + if not verifier.ask_resume(): + print("INFO: One party wanted to stop") + verifier.cleanup() + break + + for img in images: + img.close() + verifier.destroy() + system("cls") + + window.destroy() + + +if __name__ == "__main__": main() \ No newline at end of file diff --git a/verifier.py b/verifier.py new file mode 100644 index 0000000..c9c36c4 --- /dev/null +++ b/verifier.py @@ -0,0 +1,148 @@ +import tkinter as tk +from tkinter import messagebox +import io +import threading +from tkinter.constants import FALSE +from PIL import Image, ImageTk + +MAX_INIT_SIZE = 10 + +USAGE = """USAGE: Enter = Accept +USAGE: Escape = Decline +INFO: If you decline once the program will be stopped +INFO: The image will be saved only if both parties accept at all stages""" + +class Verifier(tk.Frame): + def __init__(self, master, conn, role, images): + super().__init__(master) + + self.master = master + self.conn = conn + self.role = role + self.images = images + self.stage = 0 + self.successful = False + self.paniced = False + self.busy = False + self.forcequit = False + + self.image_gui = ImageTk.PhotoImage(self.images[self.stage]) + self.active = tk.Label(self, image=self.image_gui) + self.active.pack() + + self.thread = threading.Thread(target=self.init2) + self.thread.start() + + def init2(self): + print("STATUS: Waiting for partner to finish editing...") + print("INFO: The program might become unresponsive for a while") + try: self.image = self.exchange_image(self.images[self.stage]) + except: + self.panic() + return + self.image_gui = ImageTk.PhotoImage(self.image) + self.active["image"] = self.image_gui + + self.bind("", lambda _: self.start("ACCEPT")) + self.bind("", lambda _: self.start("DECLINE")) + self.focus_force() + print(USAGE) + + def start(self, arg): + if self.busy: return + self.busy = True + self.thread = threading.Thread(target=self.verify, args=(arg,)) + self.thread.start() + + def verify(self, msg_send): + print(f"STATUS: Stage {self.stage + 1} -> {msg_send}") + try: msg_recv = self.exchange_text(msg_send) + except: + self.panic() + return + if msg_send == "ACCEPT" and msg_recv == "ACCEPT": + self.stage += 1 + else: + print("STATUS: Image was declined") + print("STATUS: Operation aborted") + self.master.quit() + if self.stage == len(self.images): + print("STATUS: Operation successful") + self.successful = True + self.master.quit() + else: + try: self.image = self.exchange_image(self.images[self.stage]) + except: + self.panic() + return + self.image_gui = ImageTk.PhotoImage(self.image) + self.active["image"] = self.image_gui + self.busy = False + + def exchange_text(self, msg_send): + msg_recv = self.exchange(msg_send.encode()) + return msg_recv.decode() + + def exchange_image(self, img_send): + fd = io.BytesIO() + img_send.save(fd, "png") + img_recv = self.exchange(fd.getvalue()) + if self.stage != 0: self.image.close() + fd.close() + return Image.open(io.BytesIO(img_recv)) + + def exchange(self, to_send): + size_send = str(len(to_send)) + size_send = "0" * (MAX_INIT_SIZE - len(size_send)) + size_send + if self.role == "client": + self.conn.sendall(size_send.encode()) + size_recv = int(self.recvall(MAX_INIT_SIZE).decode()) + self.conn.sendall(to_send) + return self.recvall(size_recv) + elif self.role == "server": + size_recv = int(self.recvall(MAX_INIT_SIZE).decode()) + self.conn.sendall(size_send.encode()) + received = self.recvall(size_recv) + self.conn.sendall(to_send) + return received + + def recvall(self, size): + msg = bytes() + while len(msg) < size: + recv = self.conn.recv(size - len(msg)) + if not recv: raise IOError + msg += recv + return msg + + def ask_resume(self): + answer = messagebox.askyesno("Question", "One more time?") + if not answer: return False + msg = "ACCEPT" if answer else "DECLINE" + print("STATUS: Waiting for partner to make up his mind...") + print("INFO: The program might become unresponsive for a while") + try: other = self.exchange_text(msg) + except: return False + return msg == "ACCEPT" and other == "ACCEPT" + + def panic(self): + if not self.forcequit: + print("ERROR: Connection lost") + print("STATUS: Operation aborted") + self.paniced = True + messagebox.showerror("Error", "Connection lost") + self.master.quit() + + def cleanup(self): + self.forcequit = True + for img in self.images: + img.close() + self.conn.close() + + def has_paniced(self): + return self.paniced + + def is_successful(self): + return self.successful + + def get_results(self): + return self.image \ No newline at end of file