Created
July 14, 2025 19:30
-
-
Save qb20nh/221d3dca1c307f6ac047734b08d93b98 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python3 | |
| import base64 | |
| import random | |
| import tkinter as tk | |
| from io import BytesIO | |
| from tkinter import filedialog, messagebox | |
| from PIL import Image | |
| from pillow_heif import register_heif_opener | |
| """ | |
| Simple meme crusher that resizes and compresses images multiple times. | |
| It uses the Pillow library to load and save images and the pillow_heif library to load HEIC images. | |
| To install the dependencies, run: | |
| ``` | |
| pip install pillow pillow-heif | |
| ``` | |
| To run the application, run: | |
| ``` | |
| python meme.py | |
| ``` | |
| """ | |
| class MemeCrusherApp(tk.Tk): | |
| def __init__(self): | |
| super().__init__() | |
| register_heif_opener() | |
| self.title("Meme Crusher") | |
| self.geometry("900x600") | |
| self.orig_img = None | |
| self.img = None | |
| self.tkimg = None | |
| self._build_ui() | |
| def _build_ui(self): | |
| controls = tk.Frame(self) | |
| controls.pack(side=tk.LEFT, fill=tk.Y, padx=10, pady=10) | |
| tk.Button(controls, text="Load Image", command=self.load_image).pack( | |
| fill=tk.X, pady=5 | |
| ) | |
| self.iter_slider = tk.Scale( | |
| controls, from_=1, to=100, orient=tk.HORIZONTAL, label="Iterations" | |
| ) | |
| self.iter_slider.set(10) | |
| self.iter_slider.pack(fill=tk.X, pady=5) | |
| self.min_scale = tk.Scale( | |
| controls, | |
| from_=0.1, | |
| to=1.0, | |
| resolution=0.01, | |
| orient=tk.HORIZONTAL, | |
| label="Min Scale", | |
| ) | |
| self.min_scale.set(0.5) | |
| self.min_scale.pack(fill=tk.X, pady=5) | |
| self.max_scale = tk.Scale( | |
| controls, | |
| from_=0.1, | |
| to=1.0, | |
| resolution=0.01, | |
| orient=tk.HORIZONTAL, | |
| label="Max Scale", | |
| ) | |
| self.max_scale.set(1.0) | |
| self.max_scale.pack(fill=tk.X, pady=5) | |
| self.qmin = tk.Scale( | |
| controls, from_=1, to=95, orient=tk.HORIZONTAL, label="Quality Min" | |
| ) | |
| self.qmin.set(10) | |
| self.qmin.pack(fill=tk.X, pady=5) | |
| self.qmax = tk.Scale( | |
| controls, from_=1, to=95, orient=tk.HORIZONTAL, label="Quality Max" | |
| ) | |
| self.qmax.set(90) | |
| self.qmax.pack(fill=tk.X, pady=5) | |
| tk.Button(controls, text="Crush!", command=self.crush).pack(fill=tk.X, pady=10) | |
| self.save_btn = tk.Button( | |
| controls, text="Save Image", state=tk.DISABLED, command=self.save_image | |
| ) | |
| self.save_btn.pack(fill=tk.X) | |
| self.display = tk.Label(self, bg="#333") | |
| self.display.pack(side=tk.RIGHT, expand=True, fill=tk.BOTH, padx=10, pady=10) | |
| def load_image(self): | |
| path = filedialog.askopenfilename( | |
| filetypes=[ | |
| ( | |
| "Image files", | |
| "*.png *.jpg *.jpeg *.bmp *.gif *.avif *.webp *.heic", | |
| ) | |
| ] | |
| ) | |
| if not path: | |
| return | |
| self.orig_img = Image.open(path).convert("RGB") | |
| self.img = self.orig_img.copy() | |
| self._show(self.img) | |
| self.save_btn.config(state=tk.DISABLED) | |
| def _show(self, img): | |
| w, h = img.size | |
| max_dim = min( | |
| self.display.winfo_width() or 400, self.display.winfo_height() or 400 | |
| ) | |
| scale = min(max_dim / w, max_dim / h, 1) | |
| disp = img.resize((int(w * scale), int(h * scale)), Image.Resampling.LANCZOS) | |
| # Encode the PIL image as PNG and display via Tkinter PhotoImage | |
| buf = BytesIO() | |
| disp.save(buf, format="PNG") | |
| buf.seek(0) | |
| b64 = base64.b64encode(buf.getvalue()) | |
| self.tkimg = tk.PhotoImage(data=b64) | |
| self.display.config(image=self.tkimg) | |
| def crush(self): | |
| if self.orig_img is None: | |
| messagebox.showwarning("No Image", "Load an image first!") | |
| return | |
| # start from original image copy | |
| img = self.orig_img.copy() | |
| for _ in range(int(self.iter_slider.get())): | |
| # determine new size based on original dimensions (non-compounding scale) | |
| scale = random.uniform(self.min_scale.get(), self.max_scale.get()) | |
| new_size = ( | |
| max(1, int(self.orig_img.width * scale)), | |
| max(1, int(self.orig_img.height * scale)), | |
| ) | |
| # resize the last compressed image to the new size | |
| resized = img.resize( | |
| new_size, | |
| random.choice( | |
| [ | |
| Image.Resampling.NEAREST, | |
| Image.Resampling.BILINEAR, | |
| Image.Resampling.BICUBIC, | |
| Image.Resampling.LANCZOS, | |
| ] | |
| ), | |
| ) | |
| # compress and reopen to chain JPEG artifacts | |
| buf = BytesIO() | |
| quality = random.randint(int(self.qmin.get()), int(self.qmax.get())) | |
| resized.save(buf, format="JPEG", quality=quality) | |
| buf.seek(0) | |
| img = Image.open(buf) | |
| self.img = img | |
| self._show(self.img) | |
| self.save_btn.config(state=tk.NORMAL) | |
| def save_image(self): | |
| if not self.img: | |
| return | |
| path = filedialog.asksaveasfilename( | |
| defaultextension=".jpg", filetypes=[("JPEG", "*.jpg")] | |
| ) | |
| if not path: | |
| return | |
| self.img.save(path, format="JPEG") | |
| messagebox.showinfo("Saved", f"Saved to {path}") | |
| if __name__ == "__main__": | |
| app = MemeCrusherApp() | |
| app.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment