Images are one of the biggest contributors to website load times. Large image files increase bandwidth consumption, slow down page rendering, and negatively affect user experience and SEO rankings.
Recently, I worked on an image optimization solution that automatically converts PNG images into WebP format using Python's Pillow library and PostgreSQL-backed storage. The results were impressive: a PNG image of 1.23 MB was reduced to just 55 KB, achieving more than 90% file size reduction while maintaining excellent visual quality.
Why Image Optimization Matters
Optimized images provide several benefits:
- Faster website loading speed
- Improved user experience
- Reduced server storage requirements
- Lower bandwidth consumption
- Better SEO performance
- Improved Core Web Vitals scores
For websites that handle thousands of images, these savings can significantly reduce infrastructure costs.
The Problem
Many applications store images in PNG format because it supports transparency and high-quality visuals. However, PNG files can become quite large, especially when uploaded directly from modern devices or design tools.
For example:
| Format | File Size |
|---|---|
| Original PNG | 1.23 MB |
| Optimized WebP | 55 KB |
This single conversion saved approximately 95% of the original file size.
The Solution: Pillow + WebP Conversion
I used Python's Pillow library to automatically convert uploaded PNG images into WebP format.
Benefits of WebP
WebP is a modern image format developed by Google that offers:
- Smaller file sizes
- High visual quality
- Transparency support
- Better compression than PNG and JPEG
- Broad browser compatibility
Sample Conversion Logic
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
from PIL import Image, ImageTk
import psycopg2
from psycopg2.extras import RealDictCursor
import uuid
import os
from io import BytesIO
from datetime import datetime
import threading
class DatabaseManager:
def __init__(self, dbname="image_compressor", user="postgres", password="12345", host="localhost", port="5432"):
"""Initialize database connection"""
self.connection_params = {
'dbname': dbname,
'user': user,
'password': password,
'host': host,
'port': port
}
self.create_table()
def get_connection(self):
"""Get database connection"""
return psycopg2.connect(**self.connection_params)
def create_table(self):
"""Create the compressed_images table if it doesn't exist"""
try:
conn = self.get_connection()
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE IF NOT EXISTS compressed_images (
id UUID PRIMARY KEY,
original_path TEXT NOT NULL,
compressed_path TEXT NOT NULL,
original_size INTEGER DEFAULT 0,
compressed_size INTEGER DEFAULT 0,
compression_quality INTEGER DEFAULT 85,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
)
""")
conn.commit()
cursor.close()
conn.close()
except Exception as e:
print(f"Error creating table: {e}")
def save_compression_record(self, original_path, compressed_path, original_size, compressed_size, quality):
"""Save compression record to database"""
try:
conn = self.get_connection()
cursor = conn.cursor()
record_id = uuid.uuid4()
cursor.execute("""
INSERT INTO compressed_images
(id, original_path, compressed_path, original_size, compressed_size, compression_quality)
VALUES (%s, %s, %s, %s, %s, %s)
""", (str(record_id), original_path, compressed_path, original_size, compressed_size, quality))
conn.commit()
cursor.close()
conn.close()
return record_id
except Exception as e:
print(f"Error saving record: {e}")
return None
def get_history(self, limit=50):
"""Get compression history"""
try:
conn = self.get_connection()
cursor = conn.cursor(cursor_factory=RealDictCursor)
cursor.execute("""
SELECT * FROM compressed_images
ORDER BY created_at DESC
LIMIT %s
""", (limit,))
records = cursor.fetchall()
cursor.close()
conn.close()
return records
except Exception as e:
print(f"Error fetching history: {e}")
return []
class ImageCompressor:
@staticmethod
def compress_to_webp(input_path, output_path, quality=85):
"""Compress image to WebP format"""
try:
img = Image.open(input_path)
# Convert RGBA/P to RGB
if img.mode in ("RGBA", "P"):
img = img.convert("RGB")
# Save as WebP
img.save(output_path, format='WEBP', quality=quality, optimize=True)
# Get file sizes
original_size = os.path.getsize(input_path)
compressed_size = os.path.getsize(output_path)
return original_size, compressed_size
except Exception as e:
raise Exception(f"Error compressing image: {str(e)}")
@staticmethod
def calculate_reduction_percentage(original_size, compressed_size):
"""Calculate size reduction percentage"""
if original_size and compressed_size:
return round(((original_size - compressed_size) / original_size) * 100, 2)
return 0
@staticmethod
def format_file_size(size_bytes):
"""Format file size to human readable format"""
for unit in ['B', 'KB', 'MB', 'GB']:
if size_bytes < 1024.0:
return f"{size_bytes:.2f} {unit}"
size_bytes /= 1024.0
return f"{size_bytes:.2f} TB"
class ImageCompressorApp:
def __init__(self, root):
self.root = root
self.root.title("Image Compressor - WebP Converter")
self.root.geometry("900x700")
self.root.resizable(False, False)
# Initialize database
self.db = DatabaseManager()
# Variables
self.selected_file = None
self.compressed_file_path = None
self.quality_var = tk.IntVar(value=60)
# Create output directory
self.output_dir = os.path.join(os.path.expanduser("~"), "CompressedImages")
os.makedirs(self.output_dir, exist_ok=True)
self.setup_ui()
def setup_ui(self):
"""Setup the user interface"""
# Title
title_frame = tk.Frame(self.root, bg="#2c3e50", height=80)
title_frame.pack(fill="x")
title_frame.pack_propagate(False)
title_label = tk.Label(
title_frame,
text="Image Compressor",
font=("Arial", 24, "bold"),
bg="#2c3e50",
fg="white"
)
title_label.pack(pady=20)
# Main container
main_frame = tk.Frame(self.root, padx=20, pady=20)
main_frame.pack(fill="both", expand=True)
# File selection section
file_frame = tk.LabelFrame(main_frame, text="Select Image", font=("Arial", 12, "bold"), padx=10, pady=10)
file_frame.pack(fill="x", pady=(0, 15))
self.file_label = tk.Label(file_frame, text="No file selected", font=("Arial", 10), fg="gray")
self.file_label.pack(side="left", padx=5)
select_btn = tk.Button(
file_frame,
text="Browse",
command=self.select_file,
bg="#3498db",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=5,
cursor="hand2"
)
select_btn.pack(side="right", padx=5)
# Quality slider section
quality_frame = tk.LabelFrame(main_frame, text="Compression Quality", font=("Arial", 12, "bold"), padx=10, pady=10)
quality_frame.pack(fill="x", pady=(0, 15))
slider_container = tk.Frame(quality_frame)
slider_container.pack(fill="x")
self.quality_label = tk.Label(slider_container, text="60", font=("Arial", 14, "bold"), fg="#2c3e50")
self.quality_label.pack(side="right", padx=10)
quality_slider = tk.Scale(
slider_container,
from_=10,
to=100,
orient="horizontal",
variable=self.quality_var,
command=self.update_quality_label,
length=400,
sliderlength=30,
font=("Arial", 9)
)
quality_slider.pack(side="left", fill="x", expand=True)
# Compress button
self.compress_btn = tk.Button(
main_frame,
text="Compress Image",
command=self.compress_image,
bg="#27ae60",
fg="white",
font=("Arial", 12, "bold"),
padx=30,
pady=10,
cursor="hand2",
state="disabled"
)
self.compress_btn.pack(pady=15)
# Results section
self.results_frame = tk.LabelFrame(main_frame, text="Compression Results", font=("Arial", 12, "bold"), padx=15, pady=15)
self.results_frame.pack(fill="both", expand=True, pady=(0, 15))
self.results_text = tk.Text(self.results_frame, height=8, font=("Arial", 10), state="disabled", wrap="word")
self.results_text.pack(fill="both", expand=True)
# Action buttons
button_frame = tk.Frame(main_frame)
button_frame.pack(fill="x")
self.open_btn = tk.Button(
button_frame,
text="Open Compressed Image",
command=self.open_compressed_file,
bg="#9b59b6",
fg="white",
font=("Arial", 10, "bold"),
padx=15,
pady=8,
cursor="hand2",
state="disabled"
)
self.open_btn.pack(side="left", padx=5)
self.save_as_btn = tk.Button(
button_frame,
text="Save As...",
command=self.save_as,
bg="#e67e22",
fg="white",
font=("Arial", 10, "bold"),
padx=15,
pady=8,
cursor="hand2",
state="disabled"
)
self.save_as_btn.pack(side="left", padx=5)
history_btn = tk.Button(
button_frame,
text="View History",
command=self.show_history,
bg="#34495e",
fg="white",
font=("Arial", 10, "bold"),
padx=15,
pady=8,
cursor="hand2"
)
history_btn.pack(side="right", padx=5)
def update_quality_label(self, value):
"""Update quality label when slider moves"""
self.quality_label.config(text=str(int(float(value))))
def select_file(self):
"""Open file dialog to select image"""
filetypes = (
('Image files', '*.jpg *.jpeg *.png'),
('JPEG files', '*.jpg *.jpeg'),
('PNG files', '*.png'),
)
filename = filedialog.askopenfilename(
title='Select an image',
filetypes=filetypes
)
if filename:
# Check file size (max 10MB)
file_size = os.path.getsize(filename)
if file_size > 10 * 1024 * 1024:
messagebox.showerror("Error", "Image file too large (> 10MB)")
return
self.selected_file = filename
self.file_label.config(text=os.path.basename(filename), fg="black")
self.compress_btn.config(state="normal")
def compress_image(self):
"""Compress the selected image"""
if not self.selected_file:
messagebox.showwarning("Warning", "Please select an image first")
return
# Disable button during compression
self.compress_btn.config(state="disabled", text="Compressing...")
self.root.update()
def compress_task():
try:
# Generate output filename
output_filename = f"compressed_{uuid.uuid4()}.webp"
output_path = os.path.join(self.output_dir, output_filename)
# Compress image
quality = self.quality_var.get()
original_size, compressed_size = ImageCompressor.compress_to_webp(
self.selected_file,
output_path,
quality
)
# Calculate reduction
reduction = ImageCompressor.calculate_reduction_percentage(original_size, compressed_size)
# Save to database
self.db.save_compression_record(
self.selected_file,
output_path,
original_size,
compressed_size,
quality
)
# Store compressed file path
self.compressed_file_path = output_path
# Update UI on main thread
self.root.after(0, lambda: self.show_results(original_size, compressed_size, reduction, output_path))
except Exception as e:
self.root.after(0, lambda: messagebox.showerror("Error", f"Compression failed: {str(e)}"))
self.root.after(0, lambda: self.compress_btn.config(state="normal", text="Compress Image"))
# Run compression in separate thread
thread = threading.Thread(target=compress_task, daemon=True)
thread.start()
def show_results(self, original_size, compressed_size, reduction, output_path):
"""Display compression results"""
self.results_text.config(state="normal")
self.results_text.delete(1.0, tk.END)
results = f"""
✓ Compression Successful!
Original Size: {ImageCompressor.format_file_size(original_size)}
Compressed Size: {ImageCompressor.format_file_size(compressed_size)}
Size Reduction: {reduction}%
Quality: {self.quality_var.get()}%
Output Location: {output_path}
"""
self.results_text.insert(1.0, results.strip())
self.results_text.config(state="disabled")
# Enable action buttons
self.open_btn.config(state="normal")
self.save_as_btn.config(state="normal")
self.compress_btn.config(state="normal", text="Compress Image")
messagebox.showinfo("Success", f"Image compressed successfully!\nSize reduced by {reduction}%")
def open_compressed_file(self):
"""Open the compressed image file"""
if self.compressed_file_path and os.path.exists(self.compressed_file_path):
os.startfile(self.compressed_file_path)
else:
messagebox.showerror("Error", "Compressed file not found")
def save_as(self):
"""Save compressed image to a custom location"""
if not self.compressed_file_path or not os.path.exists(self.compressed_file_path):
messagebox.showerror("Error", "No compressed file available")
return
save_path = filedialog.asksaveasfilename(
defaultextension=".webp",
filetypes=[("WebP files", "*.webp"), ("All files", "*.*")],
initialfile="compressed_image.webp"
)
if save_path:
try:
import shutil
shutil.copy2(self.compressed_file_path, save_path)
messagebox.showinfo("Success", f"Image saved to:\n{save_path}")
except Exception as e:
messagebox.showerror("Error", f"Failed to save file: {str(e)}")
def show_history(self):
"""Show compression history in a new window"""
history_window = tk.Toplevel(self.root)
history_window.title("Compression History")
history_window.geometry("800x500")
# Create treeview
tree_frame = tk.Frame(history_window)
tree_frame.pack(fill="both", expand=True, padx=10, pady=10)
scrollbar = tk.Scrollbar(tree_frame)
scrollbar.pack(side="right", fill="y")
columns = ("Date", "Original Size", "Compressed Size", "Reduction", "Quality")
tree = ttk.Treeview(tree_frame, columns=columns, show="headings", yscrollcommand=scrollbar.set)
scrollbar.config(command=tree.yview)
# Configure columns
tree.heading("Date", text="Date")
tree.heading("Original Size", text="Original Size")
tree.heading("Compressed Size", text="Compressed Size")
tree.heading("Reduction", text="Reduction %")
tree.heading("Quality", text="Quality %")
tree.column("Date", width=150)
tree.column("Original Size", width=120)
tree.column("Compressed Size", width=120)
tree.column("Reduction", width=100)
tree.column("Quality", width=100)
# Load history
records = self.db.get_history()
for record in records:
reduction = ImageCompressor.calculate_reduction_percentage(
record['original_size'],
record['compressed_size']
)
tree.insert("", "end", values=(
record['created_at'].strftime("%Y-%m-%d %H:%M:%S"),
ImageCompressor.format_file_size(record['original_size']),
ImageCompressor.format_file_size(record['compressed_size']),
f"{reduction}%",
f"{record['compression_quality']}%"
))
tree.pack(fill="both", expand=True)
close_btn = tk.Button(
history_window,
text="Close",
command=history_window.destroy,
bg="#e74c3c",
fg="white",
font=("Arial", 10, "bold"),
padx=20,
pady=5
)
close_btn.pack(pady=10)
def main():
root = tk.Tk()
app = ImageCompressorApp(root)
root.mainloop()
if __name__ == "__main__":
main()
This simple conversion can dramatically reduce image sizes while preserving visual quality.
Database Integration
The optimized images are stored and managed through PostgreSQL, allowing efficient tracking and retrieval of image records.
The workflow looks like this:
- User uploads a PNG image.
- Python processes the image.
- Pillow converts PNG to WebP.
- The optimized image is stored.
- PostgreSQL maintains metadata and file references.
- The website serves the compressed WebP image.
This ensures that only optimized assets are delivered to end users.
Eliminating Manual Work with a Windows Executable
Initially, the conversion script had to be executed manually.
To automate the process, I packaged the Python application into a standalone Windows executable (.exe). This allows non-technical users to run the optimization process without installing Python or executing scripts from the command line.
Advantages of the Executable
- One-click execution
- No Python installation required
- Easy deployment across multiple systems
- Suitable for daily operational use
- Reduces human error
Now the optimization process can be performed daily with minimal effort.
Performance Impact
After implementing the optimization workflow:
- Image sizes decreased by over 90%
- Page load times improved significantly
- Storage consumption reduced dramatically
- User experience improved on slower networks
- Image delivery became more efficient
Final Thoughts
Image optimization is one of the simplest and most effective ways to improve website performance. By combining Python, Pillow, PostgreSQL, and WebP conversion, it is possible to achieve massive storage and bandwidth savings with minimal development effort.
In my implementation, a 1.23 MB PNG image was reduced to just 55 KB, demonstrating the power of modern image compression techniques. Packaging the solution as a Windows executable further streamlined the workflow, making daily image optimization simple and accessible for anyone on the team.
If your application still serves large PNG files, converting them to WebP could be one of the highest-impact performance improvements you can make.
Top comments (0)