Wrzucasz na stronę piękne zdjęcia prosto z aparatu lub telefonu, a potem dziwisz się, że ładuje się ona wiekami? Winowajcą jest format. Tradycyjne pliki JPG i PNG potrafią ważyć po kilka megabajtów. Rozwiązaniem jest WebP – nowoczesny format od Google, który potrafi odchudzić grafikę nawet o 70-80% bez widocznej utraty jakości.
Kompresja obrazu to nic innego jak sprytna matematyka ukryta w kodzie. Dzieli się na dwa główne typy:
Zanim zaczniemy: otwórz terminal (CMD lub PowerShell) i zainstaluj potrzebną bibliotekę za pomocą poniższej komendy. To ona pozwoli Pythonowi otwierać i sprawnie kompresować pliki graficzne.
pip install Pillow
import os
import tkinter as tk
from tkinter import filedialog, messagebox
from io import BytesIO
from PIL import Image, ImageTk
class WebPCompressor:
def __init__(self, root):
self.root = root
self.root.title("Kamoxii WebP Compressor")
self.root.geometry("600x500")
self.image_path = None
self.original_image = None
# --- Elementy GUI ---
self.btn_load = tk.Button(
root, text="Wybierz zdjęcie (JPG/PNG)", command=self.load_image
)
self.btn_load.pack(pady=10)
self.lbl_orig_size = tk.Label(root, text="Oryginalny rozmiar: -")
self.lbl_orig_size.pack()
self.lbl_comp_size = tk.Label(root, text="Rozmiar po kompresji: -")
self.lbl_comp_size.pack(pady=5)
tk.Label(root, text="Jakość WebP (1-100):").pack()
self.slider_quality = tk.Scale(
root, from_=1, to=100, orient=tk.HORIZONTAL, command=self.update_compression
)
self.slider_quality.set(80)
self.slider_quality.pack(fill=tk.X, padx=50, pady=5)
self.lbl_preview = tk.Label(root, text="Brak podglądu")
self.lbl_preview.pack(pady=10, expand=True)
self.btn_save = tk.Button(
root,
text="Zapisz jako WebP",
command=self.save_image,
state=tk.DISABLED,
)
self.btn_save.pack(pady=10)
def load_image(self):
file_types = [("Zdjęcia", "*.jpg *.jpeg *.png *.bmp")]
self.image_path = filedialog.askopenfilename(filetypes=file_types)
if self.image_path:
self.original_image = Image.open(self.image_path)
orig_size_kb = os.path.getsize(self.image_path) / 1024
self.lbl_orig_size.config(
text=f"Oryginalny rozmiar: {orig_size_kb:.2f} KB"
)
self.btn_save.config(state=tk.NORMAL)
self.update_compression()
def update_compression(self, *args):
if not self.original_image:
return
quality = self.slider_quality.get()
buffer = BytesIO()
self.original_image.save(buffer, format="WEBP", quality=quality)
comp_size_kb = len(buffer.getvalue()) / 1024
self.lbl_comp_size.config(
text=f"Szacowany rozmiar WebP: {comp_size_kb:.2f} KB"
)
buffer.seek(0)
preview_img = Image.open(buffer)
preview_img.thumbnail((300, 300))
photo = ImageTk.PhotoImage(preview_img)
self.lbl_preview.config(image=photo, text="")
self.lbl_preview.image = photo
def save_image(self):
if not self.original_image or not self.image_path:
return
# 1. Wyciągamy samą nazwę pliku z całej ścieżki (np. "zdjecie.jpg")
base_name = os.path.basename(self.image_path)
# 2. Odcinamy stare rozszerzenie i doklejamy .webp (np. "zdjecie.webp")
name_without_ext, _ = os.path.splitext(base_name)
suggested_name = f"{name_without_ext}.webp"
# 3. Podrzucamy sugerowaną nazwę do okna zapisu przez 'initialfile'
save_path = filedialog.asksaveasfilename(
initialfile=suggested_name,
defaultextension=".webp",
filetypes=[("WebP Image", "*.webp")]
)
if save_path:
quality = self.slider_quality.get()
self.original_image.save(save_path, format="WEBP", quality=quality)
messagebox.showinfo("Sukces", "Zdjęcie zostało zapisane!")
if __name__ == "__main__":
root = tk.Tk()
app = WebPCompressor(root)
root.mainloop()
Pierwsza wersja skryptu (na samym procesorze) działa w sposób bardzo liniowy. Wykorzystujemy bibliotekę Tkinter do stworzenia okienka (GUI) oraz Pillow (PIL) do operacji na plikach.
W funkcji update_compression każde, nawet najmniejsze przesunięcie suwaka jakości zmusza Twój procesor do skompresowania całego, wielkiego zdjęcia od nowa w pamięci RAM (używając BytesIO), tylko po to, by wygenerować podgląd. Jeśli wrzucisz tam zdjęcie 24-megapikselowe, suwak zacznie potwornie lagować.
Poniższa wersja (Nvidia/Szybka) wprowadza genialny trik optymalizacyjny. Zamiast męczyć komputer przetwarzaniem gigantycznego pliku w czasie rzeczywistym:
import os
import tkinter as tk
from tkinter import filedialog, messagebox
from io import BytesIO
from PIL import Image, ImageTk
class WebPCompressor:
def __init__(self, root):
self.root = root
self.root.title("Kamoxii WebP Compressor")
self.root.geometry("600x500")
self.image_path = None
self.original_image = None
self.preview_base_img = None # Tu będziemy trzymać małą wersję zdjęcia do szybkich obliczeń
# --- Elementy GUI ---
self.btn_load = tk.Button(
root, text="Wybierz zdjęcie (JPG/PNG)", command=self.load_image
)
self.btn_load.pack(pady=10)
self.lbl_orig_size = tk.Label(root, text="Oryginalny rozmiar: -")
self.lbl_orig_size.pack()
self.lbl_comp_size = tk.Label(root, text="Rozmiar po kompresji: -")
self.lbl_comp_size.pack(pady=5)
tk.Label(root, text="Jakość WebP (1-100):").pack()
# Zmieniamy działanie suwaka: command teraz odsyła do nowej, szybkiej funkcji
self.slider_quality = tk.Scale(
root, from_=1, to=100, orient=tk.HORIZONTAL, command=self.update_compression_fast
)
self.slider_quality.set(80)
self.slider_quality.pack(fill=tk.X, padx=50, pady=5)
self.lbl_preview = tk.Label(root, text="Brak podglądu")
self.lbl_preview.pack(pady=10, expand=True)
self.btn_save = tk.Button(
root,
text="Zapisz jako WebP",
command=self.save_image,
state=tk.DISABLED,
)
self.btn_save.pack(pady=10)
def load_image(self):
file_types = [("Zdjęcia", "*.jpg *.jpeg *.png *.bmp")]
self.image_path = filedialog.askopenfilename(filetypes=file_types)
if self.image_path:
self.original_image = Image.open(self.image_path)
# --- TA CZĘŚĆ LIKWIDUJE LAGI ---
# Tworzymy leciutką kopię obrazu (max 400x400), która posłuży do błyskawicznego podglądu
self.preview_base_img = self.original_image.copy()
self.preview_base_img.thumbnail((400, 400))
# -------------------------------
orig_size_kb = os.path.getsize(self.image_path) / 1024
self.lbl_orig_size.config(
text=f"Oryginalny rozmiar: {orig_size_kb:.2f} KB"
)
self.btn_save.config(state=tk.NORMAL)
self.update_compression_fast()
def update_compression_fast(self, *args):
"""Ta funkcja wykonuje się przy każdym drgnięciu suwaka – jest ultra szybka!"""
if not self.preview_base_img:
return
quality = self.slider_quality.get()
buffer = BytesIO()
# Kompresujemy TYLKO małą miniaturkę, dzięki czemu procesor nie odczuwa żadnego wysiłku
self.preview_base_img.save(buffer, format="WEBP", quality=quality)
# Wyliczamy przybliżoną wagę (szacowaną na podstawie proporcji rozmiaru)
# Ponieważ kompresujemy miniaturę, musimy oszacować wagę dużego pliku
ratio = (self.original_image.width * self.original_image.height) / (self.preview_base_img.width * self.preview_base_img.height)
estimated_size_kb = (len(buffer.getvalue()) / 1024) * (ratio * 0.4) # 0.4 to współczynnik korygujący dla skalowania kompresji
self.lbl_comp_size.config(
text=f"Szacowany rozmiar WebP: ~{estimated_size_kb:.2f} KB"
)
buffer.seek(0)
preview_img = Image.open(buffer)
photo = ImageTk.PhotoImage(preview_img)
self.lbl_preview.config(image=photo, text="")
self.lbl_preview.image = photo
def save_image(self):
"""Ta funkcja odpala się raz – tutaj kompresujemy pełne, oryginalne zdjęcie."""
if not self.original_image or not self.image_path:
return
base_name = os.path.basename(self.image_path)
name_without_ext, _ = os.path.splitext(base_name)
suggested_name = f"{name_without_ext}.webp"
save_path = filedialog.asksaveasfilename(
initialfile=suggested_name,
defaultextension=".webp",
filetypes=[("WebP Image", "*.webp")]
)
if save_path:
# Pokazujemy kursor oczekiwania, bo pełna kompresja dużego pliku może zająć sekundę
self.root.config(cursor="wait")
self.root.update()
quality = self.slider_quality.get()
# Tutaj idzie pełna jakość i pełny rozmiar na dysk
self.original_image.save(save_path, format="WEBP", quality=quality)
self.root.config(cursor="")
messagebox.showinfo("Sukces", "Zdjęcie zostało zapisane z pełną rozdzielczością!")
if __name__ == "__main__":
root = tk.Tk()
app = WebPCompressor(root)
root.mainloop()