Files
CourseWork/gui/main_window.py
Likaon f053e2becc Внёс изменения в графический интерфейс
теперь есть возможность использовать метод DCT
2026-05-23 21:33:25 +03:00

427 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
GUI интерфейс для стеганографического приложения
"""
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import threading
import os
import sys
# Добавляем родительскую директорию для импорта модулей core
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from core.lsb import encode_lsb, decode_lsb
from core.dct import encode_dct, decode_dct
from core.utils import calculate_capacity
class StegoGUI:
"""Главное окно приложения"""
def __init__(self, root):
self.root = root
self.root.title("Stego App - Стеганографическое приложение")
self.root.geometry("800x700")
self.root.resizable(True, True)
# Переменные
self.encode_image_path = tk.StringVar()
self.encode_output_path = tk.StringVar()
self.decode_image_path = tk.StringVar()
self.method_encode = tk.StringVar(value="lsb")
self.method_decode = tk.StringVar(value="lsb")
self.setup_ui()
def setup_ui(self):
"""Создание интерфейса"""
# Вкладки
notebook = ttk.Notebook(self.root)
notebook.pack(fill='both', expand=True, padx=10, pady=10)
# Вкладка "Кодирование"
self.encode_frame = ttk.Frame(notebook)
notebook.add(self.encode_frame, text="Скрыть сообщение")
self.setup_encode_tab()
# Вкладка "Декодирование"
self.decode_frame = ttk.Frame(notebook)
notebook.add(self.decode_frame, text="Извлечь сообщение")
self.setup_decode_tab()
# Вкладка "О программе"
about_frame = ttk.Frame(notebook)
notebook.add(about_frame, text="О программе")
self.setup_about_tab(about_frame)
def setup_encode_tab(self):
"""Вкладка кодирования"""
row = 0
# Метод
ttk.Label(self.encode_frame, text="Метод стеганографии:").grid(row=row, column=0, sticky='w', padx=5, pady=5)
ttk.Radiobutton(self.encode_frame, text="LSB (рекомендуется)", variable=self.method_encode, value="lsb").grid(row=row, column=1, padx=5, sticky='w')
ttk.Radiobutton(self.encode_frame, text="DCT (для гладких/одноцветных изображений)", variable=self.method_encode, value="dct").grid(row=row, column=2, padx=5, sticky='w')
row += 1
# Разделитель
ttk.Separator(self.encode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5)
row += 1
# Исходное изображение
ttk.Label(self.encode_frame, text="Исходное изображение:").grid(row=row, column=0, sticky='w', padx=5, pady=5)
self.encode_image_entry = ttk.Entry(self.encode_frame, textvariable=self.encode_image_path, width=55)
self.encode_image_entry.grid(row=row, column=1, columnspan=2, padx=5, sticky='ew')
ttk.Button(self.encode_frame, text="Обзор...", command=self.select_encode_image).grid(row=row, column=3, padx=5)
row += 1
# Выходное изображение
ttk.Label(self.encode_frame, text="Сохранить как (PNG):").grid(row=row, column=0, sticky='w', padx=5, pady=5)
self.encode_output_entry = ttk.Entry(self.encode_frame, textvariable=self.encode_output_path, width=55)
self.encode_output_entry.grid(row=row, column=1, columnspan=2, padx=5, sticky='ew')
ttk.Button(self.encode_frame, text="Обзор...", command=self.select_encode_output).grid(row=row, column=3, padx=5)
row += 1
# Разделитель
ttk.Separator(self.encode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5)
row += 1
# Сообщение
ttk.Label(self.encode_frame, text="Сообщение для сокрытия:").grid(row=row, column=0, sticky='nw', padx=5, pady=5)
self.message_text = tk.Text(self.encode_frame, height=8, width=65, wrap=tk.WORD)
self.message_text.grid(row=row, column=1, columnspan=3, padx=5, pady=5, sticky='ew')
row += 1
# Кнопка очистки сообщения
ttk.Button(self.encode_frame, text="Очистить сообщение", command=self.clear_message).grid(row=row, column=1, sticky='w', padx=5, pady=5)
row += 1
# Разделитель
ttk.Separator(self.encode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5)
row += 1
# Вместимость
self.capacity_label = ttk.Label(self.encode_frame, text="", foreground="gray")
self.capacity_label.grid(row=row, column=1, columnspan=2, sticky='w', padx=5)
row += 1
# Прогресс
self.encode_progress = ttk.Progressbar(self.encode_frame, mode='indeterminate', length=400)
# Кнопка
self.encode_btn = ttk.Button(self.encode_frame, text="Скрыть сообщение", command=self.do_encode)
self.encode_btn.grid(row=row, column=1, columnspan=2, pady=15)
row += 1
# Статус
self.status_label = ttk.Label(self.encode_frame, text="", foreground="blue")
self.status_label.grid(row=row, column=0, columnspan=4, pady=5)
# Привязка событий
self.encode_image_path.trace('w', lambda *args: self.update_capacity())
def setup_decode_tab(self):
"""Вкладка декодирования"""
row = 0
# Метод
ttk.Label(self.decode_frame, text="Метод:").grid(row=row, column=0, sticky='w', padx=5, pady=5)
ttk.Radiobutton(self.decode_frame, text="LSB (рекомендуется)", variable=self.method_decode, value="lsb").grid(row=row, column=1, padx=5, sticky='w')
ttk.Radiobutton(self.decode_frame, text="DCT (для гладких/одноцветных изображений)", variable=self.method_decode, value="dct").grid(row=row, column=2, padx=5, sticky='w')
row += 1
# Разделитель
ttk.Separator(self.decode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5)
row += 1
# Изображение
ttk.Label(self.decode_frame, text="Изображение со скрытым сообщением:").grid(row=row, column=0, sticky='w', padx=5, pady=5)
self.decode_image_entry = ttk.Entry(self.decode_frame, textvariable=self.decode_image_path, width=55)
self.decode_image_entry.grid(row=row, column=1, columnspan=2, padx=5, sticky='ew')
ttk.Button(self.decode_frame, text="Обзор...", command=self.select_decode_image).grid(row=row, column=3, padx=5)
row += 1
# Разделитель
ttk.Separator(self.decode_frame, orient='horizontal').grid(row=row, column=0, columnspan=4, sticky='ew', padx=5, pady=5)
row += 1
# Прогресс
self.decode_progress = ttk.Progressbar(self.decode_frame, mode='indeterminate', length=400)
# Кнопка
self.decode_btn = ttk.Button(self.decode_frame, text="Извлечь сообщение", command=self.do_decode)
self.decode_btn.grid(row=row, column=1, columnspan=2, pady=15)
row += 1
# Результат
ttk.Label(self.decode_frame, text="Извлеченное сообщение:").grid(row=row, column=0, sticky='nw', padx=5, pady=5)
self.result_text = tk.Text(self.decode_frame, height=8, width=65, wrap=tk.WORD)
self.result_text.grid(row=row, column=1, columnspan=3, padx=5, pady=5, sticky='ew')
row += 1
# Кнопки
button_frame = ttk.Frame(self.decode_frame)
button_frame.grid(row=row, column=1, columnspan=2, pady=5)
self.save_btn = ttk.Button(button_frame, text="Сохранить в файл", command=self.save_result, state='disabled')
self.save_btn.pack(side='left', padx=5)
ttk.Button(button_frame, text="Очистить результат", command=self.clear_result).pack(side='left', padx=5)
row += 1
# Статус
self.decode_status = ttk.Label(self.decode_frame, text="", foreground="blue")
self.decode_status.grid(row=row, column=0, columnspan=4, pady=5)
self.decoded_message = ""
def setup_about_tab(self, frame):
"""Вкладка 'О программе'"""
# Основная информация
info_frame = ttk.Frame(frame)
info_frame.pack(pady=20)
ttk.Label(info_frame, text="Stego App", font=("Arial", 16, "bold")).pack(pady=5)
ttk.Label(info_frame, text="Стеганографическое приложение", font=("Arial", 10)).pack(pady=2)
ttk.Label(info_frame, text="").pack(pady=5)
ttk.Label(info_frame, text="Петров Игорь ИУ10-24").pack()
ttk.Label(info_frame, text="").pack(pady=5)
# Год
ttk.Label(info_frame, text="2026", font=("Arial", 10, "italic")).pack(pady=5)
# Разделитель
ttk.Separator(frame, orient='horizontal').pack(fill='x', padx=20, pady=10)
# Техническая информация
tech_frame = ttk.Frame(frame)
tech_frame.pack(pady=10)
ttk.Label(tech_frame, text="Методы стеганографии:", font=("Arial", 10, "bold")).pack(anchor='w')
ttk.Label(tech_frame, text=" • LSB (Least Significant Bit) - сокрытие в последних битах пикселей").pack(anchor='w')
ttk.Label(tech_frame, text=" Работает с любыми изображениями, высокая вместимость").pack(anchor='w', padx=20)
ttk.Label(tech_frame, text="").pack()
ttk.Label(tech_frame, text=" • DCT (Discrete Cosine Transform) - сокрытие в частотной области").pack(anchor='w')
ttk.Label(tech_frame, text=" Рекомендуется для гладких/однотонных изображений").pack(anchor='w', padx=20)
ttk.Label(tech_frame, text="").pack()
ttk.Label(tech_frame, text="Поддерживаемые форматы:", font=("Arial", 10, "bold")).pack(anchor='w')
ttk.Label(tech_frame, text=" • PNG, BMP, JPG, JPEG").pack(anchor='w')
ttk.Label(tech_frame, text="").pack()
ttk.Label(tech_frame, text="Рекомендации:", font=("Arial", 10, "bold")).pack(anchor='w')
ttk.Label(tech_frame, text=" • Для сохранения результата используйте PNG").pack(anchor='w')
ttk.Label(tech_frame, text=" • LSB - универсальный метод, рекомендуется по умолчанию").pack(anchor='w')
def select_encode_image(self):
"""Выбор изображения для кодирования"""
filename = filedialog.askopenfilename(
title="Выберите исходное изображение",
filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")]
)
if filename:
self.encode_image_path.set(filename)
def select_encode_output(self):
"""Выбор выходного файла"""
filename = filedialog.asksaveasfilename(
title="Сохранить результат как",
defaultextension=".png",
filetypes=[("PNG", "*.png"), ("BMP", "*.bmp"), ("Все файлы", "*.*")]
)
if filename:
self.encode_output_path.set(filename)
def select_decode_image(self):
"""Выбор изображения для декодирования"""
filename = filedialog.askopenfilename(
title="Выберите изображение со скрытым сообщением",
filetypes=[("Изображения", "*.png *.bmp *.jpg *.jpeg"), ("Все файлы", "*.*")]
)
if filename:
self.decode_image_path.set(filename)
def clear_message(self):
"""Очистка поля сообщения"""
self.message_text.delete("1.0", tk.END)
def clear_result(self):
"""Очистка поля результата"""
self.result_text.delete("1.0", tk.END)
self.decoded_message = ""
self.save_btn.config(state='disabled')
def update_capacity(self):
"""Обновление информации о вместимости"""
path = self.encode_image_path.get()
if path and os.path.exists(path):
try:
capacity = calculate_capacity(path)
kb = capacity / 1024
self.capacity_label.config(text=f"📊 Максимальная вместимость: {capacity} байт (~{kb:.1f} КБ)")
except Exception as e:
self.capacity_label.config(text=f"⚠️ Ошибка: {e}")
else:
self.capacity_label.config(text="")
def check_image_for_dct(self, image_path):
"""Проверка изображения для DCT метода"""
try:
from PIL import Image, ImageStat
img = Image.open(image_path)
w, h = img.size
# Проверка размеров
if w % 8 != 0 or h % 8 != 0:
return False, f"Размеры изображения ({w}x{h}) должны быть кратны 8 для DCT метода"
# Проверка "гладкости" изображения
stat = ImageStat.Stat(img)
# Если стандартное отклонение цветов большое (>50) — изображение пёстрое
if max(stat.stddev) > 50:
return False, "Изображение содержит много деталей/цветов.\nDCT рекомендуется только для гладких/однотонных изображений.\nИспользуйте LSB метод."
return True, ""
except Exception as e:
return False, str(e)
def do_encode(self):
"""Выполнение кодирования"""
image = self.encode_image_path.get()
output = self.encode_output_path.get()
message = self.message_text.get("1.0", tk.END).strip()
method = self.method_encode.get()
if not image:
messagebox.showerror("Ошибка", "Выберите исходное изображение")
return
if not output:
messagebox.showerror("Ошибка", "Укажите путь для сохранения результата")
return
if not message:
messagebox.showerror("Ошибка", "Введите сообщение для сокрытия")
return
# Проверка для DCT
if method == 'dct':
is_ok, error_msg = self.check_image_for_dct(image)
if not is_ok:
messagebox.showerror("Ошибка", error_msg)
return
# Запускаем в отдельном потоке
self.encode_btn.config(state='disabled')
self.encode_progress.grid(row=11, column=1, columnspan=2, sticky='ew', padx=5, pady=5)
self.encode_progress.start()
self.status_label.config(text="⏳ Кодирование... Пожалуйста, подождите", foreground="blue")
thread = threading.Thread(target=self._do_encode, args=(image, output, message, method))
thread.daemon = True
thread.start()
def _do_encode(self, image, output, message, method):
"""Фоновое кодирование"""
try:
if method == 'lsb':
success = encode_lsb(image, message, output)
else:
success = encode_dct(image, message, output)
self.root.after(0, self._encode_done, success, None)
except Exception as e:
self.root.after(0, self._encode_done, False, str(e))
def _encode_done(self, success, error=None):
"""Завершение кодирования"""
self.encode_progress.stop()
self.encode_progress.grid_forget()
self.encode_btn.config(state='normal')
if success:
self.status_label.config(text="✅ Кодирование успешно завершено!", foreground="green")
messagebox.showinfo("Успех", f"✅ Сообщение успешно скрыто в изображении!\n\nСохранено: {self.encode_output_path.get()}")
else:
error_msg = error or "Неизвестная ошибка"
self.status_label.config(text=f"❌ Ошибка кодирования", foreground="red")
messagebox.showerror("Ошибка", f"Не удалось скрыть сообщение:\n{error_msg}")
def do_decode(self):
"""Выполнение декодирования"""
image = self.decode_image_path.get()
method = self.method_decode.get()
if not image:
messagebox.showerror("Ошибка", "Выберите изображение")
return
# Запускаем в отдельном потоке
self.decode_btn.config(state='disabled')
self.decode_progress.grid(row=2, column=1, columnspan=2, sticky='ew', padx=5, pady=5)
self.decode_progress.start()
self.decode_status.config(text="⏳ Декодирование... Пожалуйста, подождите", foreground="blue")
thread = threading.Thread(target=self._do_decode, args=(image, method))
thread.daemon = True
thread.start()
def _do_decode(self, image, method):
"""Фоновое декодирование"""
try:
if method == 'lsb':
message = decode_lsb(image)
else:
message = decode_dct(image)
self.root.after(0, self._decode_done, message, None)
except Exception as e:
self.root.after(0, self._decode_done, None, str(e))
def _decode_done(self, message, error=None):
"""Завершение декодирования"""
self.decode_progress.stop()
self.decode_progress.grid_forget()
self.decode_btn.config(state='normal')
if message and not error and message != "":
self.decoded_message = message
self.result_text.delete("1.0", tk.END)
self.result_text.insert("1.0", message)
self.save_btn.config(state='normal')
self.decode_status.config(text="✅ Декодирование успешно завершено!", foreground="green")
messagebox.showinfo("Успех", f"✅ Сообщение успешно извлечено!\n\nДлина: {len(message)} символов")
else:
error_msg = error or "Сообщение не найдено или повреждено"
self.decode_status.config(text=f"❌ Ошибка декодирования", foreground="red")
self.save_btn.config(state='disabled')
messagebox.showerror("Ошибка", f"Не удалось извлечь сообщение:\n{error_msg}")
def save_result(self):
"""Сохранение извлеченного сообщения в файл"""
if not self.decoded_message:
return
filename = filedialog.asksaveasfilename(
title="Сохранить сообщение",
defaultextension=".txt",
filetypes=[("Текстовые файлы", "*.txt"), ("Все файлы", "*.*")]
)
if filename:
try:
with open(filename, 'w', encoding='utf-8') as f:
f.write(self.decoded_message)
messagebox.showinfo("Успех", f"✅ Сообщение сохранено в:\n{filename}")
except Exception as e:
messagebox.showerror("Ошибка", f"Не удалось сохранить файл:\n{e}")
def main():
"""Запуск GUI"""
root = tk.Tk()
app = StegoGUI(root)
root.mainloop()
if __name__ == '__main__':
main()