Dlaczego Twoja strona muli? Czas na WebP!

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.

Czym właściwie jest kompresja i jak działa?

Kompresja obrazu to nic innego jak sprytna matematyka ukryta w kodzie. Dzieli się na dwa główne typy:

  • Kompresja bezstratna (Lossless): Działa jak spakowanie ubrań do walizki próżniowej. Wypychamy "powietrze" (powtarzające się dane), ale po rozpakowaniu wszystko jest identyczne. Tak działa format PNG.
  • Kompresja stratna (Lossy): Tutaj algorytm oszukuje ludzkie oko. Usuwa detale i subtelne przejścia tonalne, których i tak nie zauważysz na ekranie telefonu czy laptopa. Tak działa JPG oraz nasz dzisiejszy bohater – WebP, który robi to po prostu o niebo skuteczniej.
💡 Ciekawostka: Algorytm WebP analizuje sąsiadujące bloki pikseli i próbuje "przewidzieć" ich zawartość, zapisując do pliku tylko różnice między nimi. To dlatego pliki końcowe są tak niesamowicie lekkie.

Krok 1: Instalacja "silnika"

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 INSTALLER
pip install Pillow
💡 Pro Tip: Jeśli masz kartę graficzną NVIDIA i chcesz, aby przetwarzanie obrazu działo błyskawicznie, upewnij się, że masz zainstalowane sterowniki CUDA. Dzięki temu Python zamiast męczyć procesor (CPU), zrzuci całą ciężką pracę bezpośrednio na rdzenie Twojej karty graficznej.



Wersja na samym procesorze (wolne)

AI-KERNEL-V1.0.exe



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()

    

Jak działa powyższy kod i dlaczego powstała wersja V2?

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.

Gdzie pojawia się problem?

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ć.

Rozwiązanie: Optymalizacja i asynchroniczne podejście w wersji V2

Poniższa wersja (Nvidia/Szybka) wprowadza genialny trik optymalizacyjny. Zamiast męczyć komputer przetwarzaniem gigantycznego pliku w czasie rzeczywistym:

  1. Podczas ładowania zdjęcia tworzymy jego leciutką miniaturkę (Thumbnail).
  2. Gdy ruszasz suwakiem, kompresujemy tylko tę miniaturkę – dzieje się to w ułamku milisekundy, dając natychmiastowy podgląd bez żadnych ścięć!
  3. Dopiero w momencie kliknięcia "Zapisz", program bierze oryginalny, ciężki plik i jednorazowo przetwarza go w pełnej rozdzielczości.

Wersja na karcie graficznej Nvidia (szybkie):

AI-KERNEL-V1.0.exe



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()
    

Data publikacji: 11.06.2026 r. | Autor: kamoxii