Простой оптимизатор изображений (+исходный код)

Привет! Вместе с ChatGPT я разработал простое в использовании приложение для оптимизации изображений, которое может быть полезно тем, кто хочет быстро уменьшить размер файлов без заметной потери качества. Возможно, оно пригодится именно вам!

Мое приложение – это легкий оптимизатор изображений, который быстро справляется со своей задачей, не нагружая систему, как Photoshop или Lightroom. Оно идеально подходит, когда хочется быстро оптимизировать фото без лишних сложностей и ненужных функций.

Простой оптимизатор изображений (+исходный код)

Что делает приложение?

Приложение позволяет:

  • Выбирать формат изображения: поддерживаются JPEG, PNG и WEBP.
  • Настраивать качество сжатия: интуитивный ползунок позволяет выбрать нужное качество, что влияет на баланс между визуальным качеством и размером файла.
  • Изменять разрешение: с помощью коэффициента уменьшения можно адаптировать изображение под нужды веб-сайтов или мобильных приложений.
  • Обрабатывать файлы в двух режимах: Одиночный режим: оптимизируйте отдельное изображение, просматривайте его исходные и итоговые параметры (разрешение, размер файла, процент снижения) и сохраняйте результат. Пакетный режим: обрабатывайте целую папку изображений и получайте сводную информацию о том, как изменился общий вес файлов после оптимизации.

Как это работает?

Программа написана на Python с использованием Tkinter для графического интерфейса и библиотеки Pillow для обработки изображений. Приложение сопровождает процесс оптимизации подробными логами, отображаемыми в специальном окне, а также сохраняет их в файл лога. Это помогает отслеживать выполнение операций и обнаруживать возможные ошибки.

Почему это может быть полезно?

Многие изображения, особенно для веб-ресурсов, бывают слишком тяжёлыми, что негативно сказывается на скорости загрузки и общем пользовательском опыте. Этот оптимизатор помогает снизить размер файлов, сохраняя при этом качество изображения на приемлемом уровне. Это особенно актуально для фотографов, веб-разработчиков и всех, кто работает с графикой.

Надеюсь, этот простой оптимизатор изображений станет для вас полезным инструментом, облегчающим работу с файлами и повышающим производительность ваших проектов!

Скачать тут:

И код (если не хочется скачивать экзешник):

# image_optimizer.py # Эта программа создаёт графический интерфейс для оптимизации изображений. # Она использует библиотеку Tkinter для интерфейса и Pillow для работы с изображениями. # Программа поддерживает одиночную оптимизацию и пакетную обработку изображений. # Дополнительно отображается информация об исходном изображении (разрешение и вес) # и об оптимизированном изображении (разрешение, вес, процент уменьшения). # Все этапы работы логируются как в отдельном текстовом окне, так и в лог-файл "image_optimizer.log". # # Важно: В Pillow 10 и выше константа ANTIALIAS заменена на Image.Resampling.LANCZOS. import tkinter as tk from tkinter import filedialog, ttk from PIL import Image, ImageTk import threading import os import logging import io # Функция для форматирования размера файла (в байтах) в килобайты с двумя знаками после запятой def format_size(size_bytes): return f"{round(size_bytes/1024, 2)} КБ" # Настройка логирования: сообщения записываются в файл "image_optimizer.log" logging.basicConfig( filename="image_optimizer.log", level=logging.DEBUG, format="%(asctime)s - %(levelname)s - %(message)s" ) # Класс для отображения всплывающих подсказок (tooltips) class ToolTip: def __init__(self, widget, text="Информация"): self.widget = widget self.text = text self.tip_window = None widget.bind("<Enter>", self.show_tip) widget.bind("<Leave>", self.hide_tip) def show_tip(self, event=None): if self.tip_window or not self.text: return x, y, cx, cy = self.widget.bbox("insert") x = x + self.widget.winfo_rootx() + 25 y = y + cy + self.widget.winfo_rooty() + 25 self.tip_window = tw = tk.Toplevel(self.widget) tw.wm_overrideredirect(True) tw.wm_geometry("+%d+%d" % (x, y)) label = tk.Label(tw, text=self.text, justify=tk.LEFT, background="#ffffe0", relief=tk.SOLID, borderwidth=1, font=("tahoma", "8", "normal")) label.pack(ipadx=1) def hide_tip(self, event=None): if self.tip_window: self.tip_window.destroy() self.tip_window = None # Главный класс приложения class ImageOptimizerApp(tk.Tk): def __init__(self): super().__init__() self.title("Оптимизатор изображений") self.geometry("900x750") # Переменные для одиночного режима self.original_image = None # PIL Image исходного изображения self.optimized_image = None # PIL Image оптимизированного изображения self.original_file_path = "" # Путь к исходному изображению # Переменные для пакетной обработки self.input_folder = "" self.output_folder = "" # Создаём глобальные настройки self.create_global_settings_frame() # Создаем вкладки для одиночного и пакетного режимов self.notebook = ttk.Notebook(self) self.notebook.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) # Вкладка для одиночного режима self.single_frame = ttk.Frame(self.notebook) self.notebook.add(self.single_frame, text="Одиночный режим") self.create_single_mode_frame() # Вкладка для пакетного режима self.batch_frame = ttk.Frame(self.notebook) self.notebook.add(self.batch_frame, text="Пакетный режим") self.create_batch_mode_frame() # Область для логирования self.create_log_frame() def create_global_settings_frame(self): frame = ttk.LabelFrame(self, text="Глобальные настройки") frame.pack(fill=tk.X, padx=10, pady=5) # Выбор формата сжатия: JPEG, PNG, WEBP self.format_var = tk.StringVar(value="JPEG") ttk.Label(frame, text="Формат:").grid(row=0, column=0, padx=5, pady=5, sticky="w") formats = [("JPEG", "JPEG"), ("PNG", "PNG"), ("WEBP", "WEBP")] col = 1 for text, mode in formats: rb = ttk.Radiobutton(frame, text=text, variable=self.format_var, value=mode) rb.grid(row=0, column=col, padx=5, pady=5, sticky="w") col += 1 # Ползунок для настройки качества сжатия (1-100) ttk.Label(frame, text="Качество:").grid(row=1, column=0, padx=5, pady=5, sticky="w") self.quality_scale = tk.Scale(frame, from_=1, to=100, orient=tk.HORIZONTAL) self.quality_scale.set(85) self.quality_scale.grid(row=1, column=1, columnspan=3, padx=5, pady=5, sticky="w") ToolTip(self.quality_scale, "Более высокие значения сохраняют больше деталей, но увеличивают размер файла. Более низкие значения уменьшают размер за счёт потерь.") # Ползунок для уменьшения разрешения (коэффициент) ttk.Label(frame, text="Коэффициент уменьшения разрешения:").grid(row=2, column=0, padx=5, pady=5, sticky="w") self.resolution_scale = tk.Scale(frame, from_=1, to=5, orient=tk.HORIZONTAL) self.resolution_scale.set(1) self.resolution_scale.grid(row=2, column=1, columnspan=3, padx=5, pady=5, sticky="w") def create_single_mode_frame(self): # Фрейм с кнопками control_frame = ttk.Frame(self.single_frame) control_frame.pack(fill=tk.X, padx=10, pady=10) load_button = ttk.Button(control_frame, text="Загрузить изображение", command=self.load_image) load_button.grid(row=0, column=0, padx=5, pady=5) optimize_button = ttk.Button(control_frame, text="Оптимизировать", command=self.optimize_image) optimize_button.grid(row=0, column=1, padx=5, pady=5) self.save_button = ttk.Button(control_frame, text="Сохранить изображение", command=self.save_image, state=tk.DISABLED) self.save_button.grid(row=0, column=2, padx=5, pady=5) clear_button = ttk.Button(control_frame, text="Очистить", command=self.clear_single_mode) clear_button.grid(row=0, column=3, padx=5, pady=5) # Предпросмотр изображения self.single_preview_label = ttk.Label(self.single_frame) self.single_preview_label.pack(padx=10, pady=10) # Метки для отображения информации об изображении self.single_info_label = ttk.Label(self.single_frame, text="", foreground="blue") self.single_info_label.pack(padx=10, pady=2) self.single_opt_info_label = ttk.Label(self.single_frame, text="", foreground="green") self.single_opt_info_label.pack(padx=10, pady=2) def create_batch_mode_frame(self): # Фрейм для выбора папок folder_frame = ttk.Frame(self.batch_frame) folder_frame.pack(fill=tk.X, padx=10, pady=10) input_button = ttk.Button(folder_frame, text="Выбрать входную папку", command=self.select_input_folder) input_button.grid(row=0, column=0, padx=5, pady=5) self.input_folder_label = ttk.Label(folder_frame, text="Не выбрана") self.input_folder_label.grid(row=0, column=1, padx=5, pady=5) output_button = ttk.Button(folder_frame, text="Выбрать выходную папку", command=self.select_output_folder) output_button.grid(row=1, column=0, padx=5, pady=5) self.output_folder_label = ttk.Label(folder_frame, text="Не выбрана") self.output_folder_label.grid(row=1, column=1, padx=5, pady=5) process_button = ttk.Button(self.batch_frame, text="Запустить обработку", command=self.start_batch_processing) process_button.pack(padx=10, pady=10) self.batch_status_label = ttk.Label(self.batch_frame, text="Обработано: 0 из 0") self.batch_status_label.pack(padx=10, pady=5) # Метка для предпросмотра последнего обработанного изображения self.batch_preview_label = ttk.Label(self.batch_frame) self.batch_preview_label.pack(padx=10, pady=10) # Метка для вывода сводной информации по пакетной обработке self.batch_summary_label = ttk.Label(self.batch_frame, text="", foreground="purple") self.batch_summary_label.pack(padx=10, pady=5) def create_log_frame(self): log_frame = ttk.LabelFrame(self, text="Лог работы") log_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=5) self.log_text = tk.Text(log_frame, height=10) self.log_text.pack(side=tk.LEFT, fill=tk.BOTH, expand=True) scrollbar = ttk.Scrollbar(log_frame, orient=tk.VERTICAL, command=self.log_text.yview) scrollbar.pack(side=tk.RIGHT, fill=tk.Y) self.log_text.config(yscrollcommand=scrollbar.set) def log_message(self, message): logging.info(message) self.log_text.insert(tk.END, message + "\n") self.log_text.see(tk.END) def load_image(self): file_path = filedialog.askopenfilename( title="Выберите изображение", filetypes=[("Image Files", "*.jpg *.jpeg *.png *.webp")] ) if file_path: try: self.original_image = Image.open(file_path) self.original_file_path = file_path self.log_message(f"Загружено изображение: {file_path}") self.show_image_preview(self.original_image, mode="single") # Получаем информацию об исходном изображении orig_width, orig_height = self.original_image.size orig_size = os.path.getsize(file_path) info_text = f"Исходное изображение: {orig_width}x{orig_height} пикселей, вес: {format_size(orig_size)}" self.single_info_label.config(text=info_text) # Сбрасываем информацию по оптимизированному изображению self.single_opt_info_label.config(text="") except Exception as e: self.log_message(f"Ошибка загрузки изображения: {e}") def show_image_preview(self, image, mode="single"): preview_image = image.copy() preview_image.thumbnail((400, 400)) photo = ImageTk.PhotoImage(preview_image) if mode == "single": self.single_preview_label.config(image=photo) self.single_preview_label.image = photo elif mode == "batch": self.batch_preview_label.config(image=photo) self.batch_preview_label.image = photo def optimize_image(self): if self.original_image is None: self.log_message("Сначала загрузите изображение для оптимизации.") return try: format_choice = self.format_var.get() quality = self.quality_scale.get() resolution_factor = self.resolution_scale.get() self.log_message(f"Оптимизация изображения: формат={format_choice}, качество={quality}, коэффициент уменьшения={resolution_factor}") # Изменяем разрешение изображения new_width = self.original_image.width // resolution_factor new_height = self.original_image.height // resolution_factor optimized = self.original_image.resize((new_width, new_height), Image.Resampling.LANCZOS) # Сохраняем оптимизированное изображение во временный буфер buffer = io.BytesIO() if format_choice == "JPEG": optimized.save(buffer, format=format_choice, quality=quality) else: optimized.save(buffer, format=format_choice) optimized_data = buffer.getvalue() buffer.seek(0) self.optimized_image = Image.open(buffer) # Вычисляем размеры файлов orig_size = os.path.getsize(self.original_file_path) opt_size = len(optimized_data) reduction = round((1 - opt_size/orig_size) * 100, 2) info_text = f"Оптимизированное изображение: {new_width}x{new_height} пикселей, вес: {format_size(opt_size)}, снижение размера: {reduction}%" self.single_opt_info_label.config(text=info_text) self.log_message("Изображение успешно оптимизировано.") self.show_image_preview(self.optimized_image, mode="single") self.save_button.config(state=tk.NORMAL) except Exception as e: self.log_message(f"Ошибка при оптимизации изображения: {e}") def save_image(self): if self.optimized_image is None: self.log_message("Нет оптимизированного изображения для сохранения.") return format_choice = self.format_var.get() ext = format_choice.lower() if format_choice != "JPEG" else "jpg" file_path = filedialog.asksaveasfilename( defaultextension=f".{ext}", filetypes=[(f"{format_choice} files", f"*.{ext}")] ) if file_path: try: quality = self.quality_scale.get() if format_choice == "JPEG": self.optimized_image.save(file_path, format=format_choice, quality=quality) else: self.optimized_image.save(file_path, format=format_choice) self.log_message(f"Изображение сохранено: {file_path}") except Exception as e: self.log_message(f"Ошибка сохранения изображения: {e}") def clear_single_mode(self): self.original_image = None self.optimized_image = None self.original_file_path = "" self.single_preview_label.config(image="") self.save_button.config(state=tk.DISABLED) self.single_info_label.config(text="") self.single_opt_info_label.config(text="") self.log_message("Одиночный режим очищен.") def select_input_folder(self): folder = filedialog.askdirectory(title="Выберите входную папку") if folder: self.input_folder = folder self.input_folder_label.config(text=folder) self.log_message(f"Выбрана входная папка: {folder}") def select_output_folder(self): folder = filedialog.askdirectory(title="Выберите выходную папку") if folder: self.output_folder = folder self.output_folder_label.config(text=folder) self.log_message(f"Выбрана выходная папка: {folder}") def start_batch_processing(self): if not self.input_folder or not self.output_folder: self.log_message("Необходимо выбрать входную и выходную папки.") return # Сброс сводной информации self.batch_summary_label.config(text="") thread = threading.Thread(target=self.batch_process) thread.start() def batch_process(self): try: files = os.listdir(self.input_folder) image_extensions = (".jpg", ".jpeg", ".png", ".webp") image_files = [f for f in files if f.lower().endswith(image_extensions)] total_files = len(image_files) processed_count = 0 if total_files == 0: self.log_message("В выбранной папке не найдено изображений.") return # Суммарные размеры исходных и оптимизированных файлов total_original_size = 0 total_optimized_size = 0 self.log_message(f"Начинается пакетная обработка {total_files} изображений.") for filename in image_files: try: file_path = os.path.join(self.input_folder, filename) image = Image.open(file_path) orig_size = os.path.getsize(file_path) total_original_size += orig_size format_choice = self.format_var.get() quality = self.quality_scale.get() resolution_factor = self.resolution_scale.get() new_width = image.width // resolution_factor new_height = image.height // resolution_factor optimized = image.resize((new_width, new_height), Image.Resampling.LANCZOS) # Сохраняем оптимизированное изображение во временный буфер buffer = io.BytesIO() if format_choice == "JPEG": optimized.save(buffer, format=format_choice, quality=quality) else: optimized.save(buffer, format=format_choice) optimized_data = buffer.getvalue() opt_size = len(optimized_data) total_optimized_size += opt_size # Записываем оптимизированное изображение на диск output_filename = os.path.splitext(filename)[0] if format_choice == "JPEG": output_filename += ".jpg" else: output_filename += f".{format_choice.lower()}" output_path = os.path.join(self.output_folder, output_filename) with open(output_path, "wb") as f: f.write(optimized_data) processed_count += 1 self.after(0, self.update_batch_status, processed_count, total_files) self.after(0, self.show_image_preview, optimized, "batch") self.log_message(f"Обработано изображение: {filename}") except Exception as e: self.log_message(f"Ошибка обработки файла {filename}: {e}") # Вычисляем процентное снижение размера для всей партии reduction = round((1 - total_optimized_size/total_original_size) * 100, 2) if total_original_size > 0 else 0 summary_text = (f"Обработка завершена.\n" f"Исходный общий размер: {format_size(total_original_size)}\n" f"Оптимизированный общий размер: {format_size(total_optimized_size)}\n" f"Снижение размера: {reduction}%") self.after(0, self.batch_summary_label.config, {"text": summary_text}) self.log_message("Пакетная обработка завершена.") except Exception as e: self.log_message(f"Ошибка пакетной обработки: {e}") def update_batch_status(self, processed, total): self.batch_status_label.config(text=f"Обработано: {processed} из {total}") if __name__ == "__main__": app = ImageOptimizerApp() app.mainloop()
6
3
3
10 комментариев