import tkinter as tk from tkinter import ttk, filedialog, scrolledtext, messagebox from PIL import Image, ImageTk, ImageEnhance, ImageOps import subprocess import os class UNLKeychainProgrammerApp: def __init__(self, root): self.root = root self.root.title("UNL Keychain Utility") self.root.geometry("800x600") # Create a notebook (tabbed interface) self.notebook = ttk.Notebook(root) self.notebook.pack(fill=tk.BOTH, expand=True) # Page 1: UNL Keychain Programmer self.page1 = ttk.Frame(self.notebook) self.notebook.add(self.page1, text="Keychain Programmer") # Terminal output window self.terminal_output = scrolledtext.ScrolledText(self.page1, wrap=tk.WORD, width=80, height=20) self.terminal_output.pack(padx=10, pady=10) # Add initial text to the terminal output window self.terminal_output.insert(tk.END, "> Welcome to the UNL Keychain Utility application v1.0.\n") self.terminal_output.insert(tk.END, "> To begin programming your keychain, select the '.bin' file from the 'build' folder.\n") self.terminal_output.insert(tk.END, "> Create custom images using the Bitmap Playground tab.\n\n") # Select File button self.select_file_button = ttk.Button(self.page1, text="Select File", command=self.select_file) self.select_file_button.pack(pady=5) # Flash Firmware button self.flash_firmware_button = ttk.Button(self.page1, text="Flash Firmware", command=self.flash_firmware) self.flash_firmware_button.pack(pady=5) # Button to toggle options visibility self.toggle_options_button = ttk.Button(self.page1, text="Options", command=self.toggle_options) self.toggle_options_button.pack(pady=5) # Frame to hold checkboxes (initially hidden) self.options_frame = ttk.Frame(self.page1) self.options_visible = False # Checkboxes for options self.options_vars = {} for option in ["-u", "-l", "-e", "-o", "-G", "-R"]: var = tk.BooleanVar() self.options_vars[option] = var checkbox = ttk.Checkbutton(self.options_frame, text=option, variable=var) checkbox.pack(anchor=tk.W) # Page 2: Image to Bitmap Converter self.page2 = ttk.Frame(self.notebook) self.notebook.add(self.page2, text="Bitmap Playground") # Upload Image button self.upload_image_button = ttk.Button(self.page2, text="Upload Image", command=self.upload_image) self.upload_image_button.pack(pady=10) # Brightness label self.brightness_label = ttk.Label(self.page2, text="Brightness:") self.brightness_label.pack(pady=5) # Brightness slider with labels self.brightness_frame = ttk.Frame(self.page2) self.brightness_frame.pack(pady=5) self.brightness_label_left = ttk.Label(self.brightness_frame, text="0%") self.brightness_label_left.pack(side=tk.LEFT, padx=5) self.brightness_slider = ttk.Scale(self.brightness_frame, from_=0, to=1, value=1, orient=tk.HORIZONTAL) self.brightness_slider.pack(side=tk.LEFT, fill=tk.X, expand=True) self.brightness_label_right = ttk.Label(self.brightness_frame, text="100%") self.brightness_label_right.pack(side=tk.LEFT, padx=5) # Mono Mode button self.mono_mode_var = tk.BooleanVar() self.mono_mode_button = ttk.Checkbutton(self.page2, text="Mono Mode", variable=self.mono_mode_var, command=self.toggle_mono_mode) self.mono_mode_button.pack(pady=5) # Generate Image button self.generate_image_button = ttk.Button(self.page2, text="Generate Bitmap", command=self.generate_image) self.generate_image_button.pack(pady=10) # Image preview window self.image_preview_label = ttk.Label(self.page2) self.image_preview_label.pack(pady=10) # C Array output text box self.c_array_text = scrolledtext.ScrolledText(self.page2, wrap=tk.WORD, width=100, height=13) self.c_array_text.pack(padx=10, pady=10) # Add a "Copy to Clipboard" button self.copy_button = ttk.Button(self.page2, text="Copy Code to Clipboard", command=self.copy_to_clipboard) self.copy_button.pack(pady=10) # Page 3: GIF to Bitmap Converter self.page3 = ttk.Frame(self.notebook) self.notebook.add(self.page3, text="GIF Playground") # Upload GIF button self.upload_gif_button = ttk.Button(self.page3, text="Upload GIF", command=self.upload_gif) self.upload_gif_button.pack(pady=10) # Label to display the number of frames self.frame_count_label = ttk.Label(self.page3, text="Original number of frames: 0") self.frame_count_label.pack(pady=5) # Label to display the slider value (frames to output) self.frame_slider_value_label = ttk.Label(self.page3, text="Frames to output: 0") self.frame_slider_value_label.pack(pady=5) # Slider for number of frames self.frame_slider_label = ttk.Label(self.page3) #self.frame_slider_label.pack(pady=5) self.frame_slider = ttk.Scale(self.page3, from_=1, to=100, orient=tk.HORIZONTAL) self.frame_slider.pack(pady=5) # Bind the slider to update the label self.frame_slider.config(command=self.update_slider_value_label) # Checkbox for retaining aspect ratio self.aspect_ratio_var = tk.BooleanVar(value=True) self.aspect_ratio_checkbox = ttk.Checkbutton(self.page3, text="Retain Aspect Ratio", variable=self.aspect_ratio_var) self.aspect_ratio_checkbox.pack(pady=5) # Save GIF button self.save_gif_button = ttk.Button(self.page3, text="Generate GIF Frame Code", command=self.save_gif) self.save_gif_button.pack(pady=10) # GIF preview window self.gif_preview_label = ttk.Label(self.page3) self.gif_preview_label.pack(pady=10) # C Array output text box self.gif_array_text = scrolledtext.ScrolledText(self.page3, wrap=tk.WORD, width=100, height=13) self.gif_array_text.pack(padx=10, pady=10) # Add a "Copy to Clipboard" button self.copy_text = ttk.Button(self.page3, text="Copy Code to Clipboard", command=self.copy_to_clipboard) self.copy_text.pack(pady=10) # Variable to track the preview loop self.after_id = None def toggle_options(self): """Toggle the visibility of the options frame.""" if self.options_visible: self.options_frame.pack_forget() else: self.options_frame.pack(pady=5, fill=tk.X) self.options_visible = not self.options_visible def select_file(self): file_path = filedialog.askopenfilename(filetypes=[("Binary files", "*.bin")]) if file_path: self.terminal_output.insert(tk.END, f"Selected file: {file_path}\n") self.selected_file = file_path else: self.terminal_output.insert(tk.END, "No file selected.\n") def flash_firmware(self): if hasattr(self, 'selected_file'): selected_options = [option for option, var in self.options_vars.items() if var.get()] command = f"puyaisp {' '.join(selected_options)} -f {self.selected_file}" # Display the command being executed self.terminal_output.insert(tk.END, f"Executing command: {command}\n") try: result = subprocess.run(command, shell=True, capture_output=True, text=True) self.terminal_output.insert(tk.END, result.stdout) self.terminal_output.insert(tk.END, result.stderr) except Exception as e: self.terminal_output.insert(tk.END, f"Error: {e}\n") else: self.terminal_output.insert(tk.END, "No file selected. Please select a file first.\n") def upload_image(self): file_path = filedialog.askopenfilename(filetypes=[("Image files", "*.jpg *.png *.bmp")]) if file_path: self.image_path = file_path self.show_image_preview(file_path) else: self.image_preview_label.config(text="No image selected.") def toggle_mono_mode(self): """Enable or disable the brightness slider based on mono mode.""" if self.mono_mode_var.get(): self.brightness_slider.config(state=tk.DISABLED) # Disable the slider else: self.brightness_slider.config(state=tk.NORMAL) # Enable the slider def generate_image(self): if hasattr(self, 'image_path'): brightness = self.brightness_slider.get() if not self.mono_mode_var.get() else None use_second_method = self.mono_mode_var.get() if use_second_method: pixels = self.process_image_mono(self.image_path) else: pixels = self.process_image_regular(self.image_path, brightness) c_arr = self.generate_c_array(pixels, "const uint8_t custom_frame[1024] = {") self.c_array_text.delete(1.0, tk.END) self.c_array_text.insert(tk.END, c_arr) # Show the processed image preview base_filename = os.path.basename(self.image_path) processed_image_path = f'monochrome_{base_filename}' self.show_image_preview(processed_image_path) else: self.c_array_text.delete(1.0, tk.END) self.c_array_text.insert(tk.END, "No image selected. Please upload an image first.") def copy_to_clipboard(self): """Copy the content of the ScrolledText widget to the clipboard.""" text = self.c_array_text.get("1.0", tk.END).strip() self.root.clipboard_clear() self.root.clipboard_append(text) self.root.update() def process_image_regular(self, filename, brightness=1.0): img = Image.open(filename).convert('L') enhancer = ImageEnhance.Brightness(img) img = enhancer.enhance(brightness) img = img.resize((128, 64)) img.save('greyscale3.png') mono = img.convert('1') mono = mono.resize((128, 64)) pixels = list(mono.getdata()) white = pixels.count(255) black = pixels.count(0) if white > black: pixels = [not elem for elem in pixels] mono = Image.new(mono.mode, mono.size) mono.putdata(pixels) base_filename = os.path.basename(filename) output_filename = f'monochrome_{base_filename}' mono.save(output_filename) return pixels def process_image_mono(self, filename): img = Image.open(filename).convert('L') img = img.resize((128, 64)) threshold = 128 mono = img.point(lambda p: 255 if p > threshold else 0, '1') pixels = list(mono.getdata()) white = pixels.count(255) black = pixels.count(0) if white > black: pixels = [0 if elem == 255 else 255 for elem in pixels] mono = Image.new('1', (128, 64)) mono.putdata(pixels) base_filename = os.path.basename(filename) output_filename = f'monochrome_{base_filename}' mono.save(output_filename) return pixels def generate_c_array(self, pixels, c_arr_str): c_arr = c_arr_str q = 0 for i in range(len(pixels)): j = i % 8 if j == 0 and i != 0: c_arr += f"0x{q:02x}," q = 0 if pixels[i] == 255: q += 1 << (7 - j) c_arr += f"0x{q:02x}" c_arr += '};' start_index = c_arr.find("{") + 1 end_index = c_arr.find("}") hex_values_str = c_arr[start_index:end_index] hex_values_list = hex_values_str.split(",") reg_bitmap = [int(x.strip(), 16) for x in hex_values_list] paged_bitmap = [] for page in range(8): for segment in range(16): buff = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00] for col in range(8): b = reg_bitmap[1023 - (page * 128 + segment + col * 16)] for e in range(8): buff[e] = buff[e] | ((b & 0x01) << col) b = b >> 1 paged_bitmap.extend(buff) hex_strings = [f"0x{byte:02x}" for byte in paged_bitmap] chunks = [hex_strings[i:i + 16] for i in range(0, len(hex_strings), 16)] rows = [", ".join(chunk) for chunk in chunks] return c_arr_str + "\n " + ",\n ".join(rows) + "};" def show_image_preview(self, image_path): image = Image.open(image_path) image = image.resize((128, 64)) photo = ImageTk.PhotoImage(image) self.image_preview_label.config(image=photo) self.image_preview_label.image = photo def upload_gif(self): file_path = filedialog.askopenfilename(filetypes=[("GIF files", "*.gif")]) if file_path: self.gif_path = file_path self.show_gif_preview(file_path) else: self.gif_preview_label.config(text="No GIF selected.") def show_gif_preview(self, gif_path): if self.after_id: self.root.after_cancel(self.after_id) # Stop the previous loop gif = Image.open(gif_path) self.gif_frames = [] self.frame_durations = [] for frame in range(gif.n_frames): gif.seek(frame) self.gif_frames.append(gif.copy()) self.frame_durations.append(gif.info['duration']) self.current_frame = 0 self.frame_count_label.config(text=f"Number of frames: {len(self.gif_frames)}") # Update frame count self.frame_slider.config(to=len(self.gif_frames)) # Update slider max value self.frame_slider.set(int(len(self.gif_frames) * 0.75)) # Set default to 75% of total frames self.update_slider_value_label(self.frame_slider.get()) # Update the slider value label self.update_gif_preview() def update_slider_value_label(self, value): """Update the label to show the current slider value.""" self.frame_slider_value_label.config(text=f"Frames to output: {int(float(value))}") def update_gif_preview(self): if self.gif_frames: frame_image = ImageTk.PhotoImage(self.gif_frames[self.current_frame]) self.gif_preview_label.config(image=frame_image) self.gif_preview_label.image = frame_image self.current_frame = (self.current_frame + 1) % len(self.gif_frames) self.after_id = self.root.after(self.frame_durations[self.current_frame], self.update_gif_preview) def save_gif(self): if hasattr(self, 'gif_path'): num_frames = int(self.frame_slider.get()) # Get the slider value retain_aspect_ratio = self.aspect_ratio_var.get() output_path = os.path.join(os.getcwd(), "output.gif") self.convert_gif_to_bw_gif(self.gif_path, output_path, num_frames, retain_aspect_ratio) self.show_gif_preview(output_path) else: messagebox.showerror("Error", "No valid GIF file uploaded.") def convert_gif_to_bw_gif(self, gif_path, output_path, num_frames, retain_aspect_ratio): try: gif = Image.open(gif_path) frames = [] durations = [] total_frames = gif.n_frames step = total_frames / num_frames for i in range(num_frames): frame_index = int(i * step) gif.seek(frame_index) if retain_aspect_ratio: resized_frame = self.resize_with_aspect_ratio(gif, (128, 64)) else: resized_frame = gif.resize((128, 64)) bw_frame = resized_frame.convert("1") frames.append(bw_frame) durations.append(gif.info['duration']) frames[0].save( output_path, save_all=True, append_images=frames[1:], loop=0, duration=durations, disposal=2 ) self.gif_array_text.delete(1.0, tk.END) for idx, frame in enumerate(frames): pixels = list(frame.getdata()) c_arr = self.generate_c_array(pixels, f"const uint8_t gif{idx}[1024] = {{") self.gif_array_text.insert(tk.END, c_arr + "\n\n") #messagebox.showinfo("Success", f"GIF converted to 128x64 black-and-white GIF and saved as {output_path}") return output_path except Exception as e: messagebox.showerror("Error", f"An error occurred: {e}") return None def resize_with_aspect_ratio(self, frame, target_size): original_width, original_height = frame.size target_width, target_height = target_size aspect_ratio = original_width / original_height if aspect_ratio > (target_width / target_height): new_width = target_width new_height = int(target_width / aspect_ratio) else: new_height = target_height new_width = int(target_height * aspect_ratio) resized_frame = frame.resize((new_width, new_height)) padded_frame = Image.new("1", target_size, 0) padded_frame.paste(resized_frame, ( (target_width - new_width) // 2, (target_height - new_height) // 2 )) return padded_frame if __name__ == "__main__": root = tk.Tk() app = UNLKeychainProgrammerApp(root) root.mainloop()