Skip to content

Instantly share code, notes, and snippets.

@qb20nh
Created July 14, 2025 19:30
Show Gist options
  • Select an option

  • Save qb20nh/221d3dca1c307f6ac047734b08d93b98 to your computer and use it in GitHub Desktop.

Select an option

Save qb20nh/221d3dca1c307f6ac047734b08d93b98 to your computer and use it in GitHub Desktop.
#!/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