oopsikxd

Untitled

Dec 14th, 2025
59
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 49.06 KB | Photo | 0 0
  1. # -*- coding: utf-8 -*-
  2. """
  3. Pikabu Avatar Maker
  4. Адаптивный интерфейс с переключением темы.
  5. by KOTOFEICH
  6. """
  7.  
  8. import tkinter as tk
  9. from tkinter import ttk, filedialog, messagebox
  10. from PIL import Image, ImageTk, ImageDraw
  11. import os
  12. import re
  13. import sys
  14. import webbrowser
  15. import platform
  16. import tempfile
  17. import atexit
  18.  
  19.  
  20. class SingleInstance:
  21. """Класс для предотвращения повторного запуска программы."""
  22.  
  23. def __init__(self, app_name: str = "PikabuAvatarMaker"):
  24. self.lockfile = os.path.join(tempfile.gettempdir(), f"{app_name}.lock")
  25. self.fd = None
  26.  
  27. def is_running(self) -> bool:
  28. """Проверяет, запущен ли уже экземпляр программы."""
  29. if platform.system() == 'Windows':
  30. return self._check_windows()
  31. else:
  32. return self._check_unix()
  33.  
  34. def _check_windows(self) -> bool:
  35. """Проверка для Windows."""
  36. try:
  37. # Пытаемся открыть файл в эксклюзивном режиме
  38. if os.path.exists(self.lockfile):
  39. os.remove(self.lockfile)
  40. self.fd = open(self.lockfile, 'w')
  41. self.fd.write(str(os.getpid()))
  42. self.fd.flush()
  43. return False # Не запущен
  44. except (IOError, OSError, PermissionError):
  45. return True # Уже запущен
  46.  
  47. def _check_unix(self) -> bool:
  48. """Проверка для Linux/macOS."""
  49. import fcntl
  50. try:
  51. self.fd = open(self.lockfile, 'w')
  52. fcntl.flock(self.fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
  53. self.fd.write(str(os.getpid()))
  54. self.fd.flush()
  55. return False # Не запущен
  56. except (IOError, OSError):
  57. return True # Уже запущен
  58.  
  59. def cleanup(self) -> None:
  60. """Очистка при закрытии программы."""
  61. try:
  62. if self.fd:
  63. self.fd.close()
  64. if os.path.exists(self.lockfile):
  65. os.remove(self.lockfile)
  66. except:
  67. pass
  68.  
  69.  
  70. class ThemeManager:
  71. """Менеджер тем оформления."""
  72.  
  73. LIGHT_THEME = {
  74. 'name': 'light',
  75. 'bg': '#ffffff',
  76. 'fg': '#000000',
  77. 'accent': '#7bc143',
  78. 'canvas_bg': '#f5f5f5',
  79. 'frame_bg': '#ffffff',
  80. 'entry_bg': '#ffffff',
  81. 'entry_fg': '#000000',
  82. 'button_bg': '#e1e1e1',
  83. 'button_fg': '#000000',
  84. 'accent_button_bg': '#7bc143',
  85. 'accent_button_fg': '#ffffff',
  86. 'link_color': '#7bc143',
  87. 'border': '#cccccc',
  88. 'debug_fg': '#666666',
  89. 'disabled_fg': '#999999',
  90. 'slider_bg': '#7bc143',
  91. }
  92.  
  93. DARK_THEME = {
  94. 'name': 'dark',
  95. 'bg': '#1a1a1a',
  96. 'fg': '#e0e0e0',
  97. 'accent': '#7bc143',
  98. 'canvas_bg': '#2d2d2d',
  99. 'frame_bg': '#252525',
  100. 'entry_bg': '#3d3d3d',
  101. 'entry_fg': '#e0e0e0',
  102. 'button_bg': '#4a4a4a',
  103. 'button_fg': '#e0e0e0',
  104. 'accent_button_bg': '#7bc143',
  105. 'accent_button_fg': '#ffffff',
  106. 'link_color': '#8fd14f',
  107. 'border': '#555555',
  108. 'debug_fg': '#888888',
  109. 'disabled_fg': '#666666',
  110. 'slider_bg': '#7bc143',
  111. }
  112.  
  113. def __init__(self, root: tk.Tk):
  114. self.root = root
  115. self.style = ttk.Style()
  116. self.current_theme = self.LIGHT_THEME
  117. self._theme_var = tk.BooleanVar(value=False)
  118.  
  119. self.is_windows = platform.system() == 'Windows'
  120.  
  121. available_themes = self.style.theme_names()
  122. if 'clam' in available_themes:
  123. self.style.theme_use('clam')
  124. elif 'alt' in available_themes:
  125. self.style.theme_use('alt')
  126.  
  127. @property
  128. def is_dark(self) -> bool:
  129. return self._theme_var.get()
  130.  
  131. @property
  132. def theme_var(self) -> tk.BooleanVar:
  133. return self._theme_var
  134.  
  135. def apply_theme(self, dark: bool = False) -> dict:
  136. """Применяет тему и возвращает её параметры."""
  137. self.current_theme = self.DARK_THEME if dark else self.LIGHT_THEME
  138. theme = self.current_theme
  139.  
  140. # === Базовые стили ===
  141. self.style.configure('.',
  142. background=theme['bg'],
  143. foreground=theme['fg'],
  144. fieldbackground=theme['entry_bg'],
  145. troughcolor=theme['bg'],
  146. borderwidth=1,
  147. focuscolor=theme['accent']
  148. )
  149.  
  150. # === Frame ===
  151. self.style.configure('TFrame', background=theme['bg'])
  152.  
  153. # === LabelFrame ===
  154. self.style.configure('TLabelframe', background=theme['bg'])
  155. self.style.configure('TLabelframe.Label',
  156. background=theme['bg'],
  157. foreground=theme['fg'],
  158. font=('Arial', 10, 'bold')
  159. )
  160.  
  161. # === Label ===
  162. self.style.configure('TLabel',
  163. background=theme['bg'],
  164. foreground=theme['fg']
  165. )
  166.  
  167. # === Entry ===
  168. self.style.configure('TEntry',
  169. fieldbackground=theme['entry_bg'],
  170. foreground=theme['entry_fg'],
  171. insertcolor=theme['entry_fg'],
  172. bordercolor=theme['border'],
  173. lightcolor=theme['entry_bg'],
  174. darkcolor=theme['entry_bg']
  175. )
  176. self.style.map('TEntry',
  177. fieldbackground=[
  178. ('focus', theme['entry_bg']),
  179. ('!focus', theme['entry_bg'])
  180. ],
  181. foreground=[
  182. ('focus', theme['entry_fg']),
  183. ('!focus', theme['entry_fg'])
  184. ],
  185. bordercolor=[
  186. ('focus', theme['accent']),
  187. ('!focus', theme['border'])
  188. ]
  189. )
  190.  
  191. # === Обычная кнопка ===
  192. self.style.configure('TButton',
  193. background=theme['button_bg'],
  194. foreground=theme['button_fg'],
  195. bordercolor=theme['border'],
  196. lightcolor=theme['button_bg'],
  197. darkcolor=theme['button_bg'],
  198. padding=(10, 5)
  199. )
  200. self.style.map('TButton',
  201. background=[
  202. ('active', theme['accent']),
  203. ('pressed', theme['accent']),
  204. ('disabled', theme['bg'])
  205. ],
  206. foreground=[
  207. ('active', '#ffffff'),
  208. ('pressed', '#ffffff'),
  209. ('disabled', theme['disabled_fg'])
  210. ]
  211. )
  212.  
  213. # === Акцентная кнопка ===
  214. self.style.configure('Accent.TButton',
  215. background=theme['accent_button_bg'],
  216. foreground=theme['accent_button_fg'],
  217. bordercolor=theme['accent_button_bg'],
  218. lightcolor=theme['accent_button_bg'],
  219. darkcolor=theme['accent_button_bg'],
  220. padding=(20, 10),
  221. font=('Arial', 11, 'bold')
  222. )
  223. self.style.map('Accent.TButton',
  224. background=[
  225. ('active', '#8fd14f'),
  226. ('pressed', '#6aa832'),
  227. ('disabled', theme['button_bg'])
  228. ],
  229. foreground=[
  230. ('active', '#ffffff'),
  231. ('pressed', '#ffffff'),
  232. ('disabled', theme['disabled_fg'])
  233. ]
  234. )
  235.  
  236. # === Checkbutton ===
  237. self.style.configure('TCheckbutton',
  238. background=theme['bg'],
  239. foreground=theme['fg']
  240. )
  241. self.style.map('TCheckbutton',
  242. background=[('active', theme['bg'])],
  243. foreground=[('active', theme['accent'])]
  244. )
  245.  
  246. # === Radiobutton ===
  247. self.style.configure('TRadiobutton',
  248. background=theme['bg'],
  249. foreground=theme['fg']
  250. )
  251. self.style.map('TRadiobutton',
  252. background=[('active', theme['bg'])],
  253. foreground=[('active', theme['accent'])]
  254. )
  255.  
  256. # === Scale (Slider) ===
  257. self.style.configure('Horizontal.TScale',
  258. background=theme['bg'],
  259. troughcolor=theme['border'],
  260. sliderthickness=20
  261. )
  262. self.style.map('Horizontal.TScale',
  263. background=[('active', theme['accent'])]
  264. )
  265.  
  266. # === Separator ===
  267. self.style.configure('TSeparator', background=theme['border'])
  268.  
  269. # === Специальные стили ===
  270.  
  271. # Ссылка
  272. self.style.configure('Link.TLabel',
  273. background=theme['bg'],
  274. foreground=theme['link_color'],
  275. font=('Arial', 10, 'underline')
  276. )
  277.  
  278. # Заголовок
  279. self.style.configure('Title.TLabel',
  280. background=theme['bg'],
  281. foreground=theme['accent'],
  282. font=('Arial', 16, 'bold')
  283. )
  284.  
  285. # Debug
  286. self.style.configure('Debug.TLabel',
  287. background=theme['bg'],
  288. foreground=theme['debug_fg'],
  289. font=('Consolas', 9)
  290. )
  291.  
  292. # Offset label
  293. self.style.configure('Offset.TLabel',
  294. background=theme['bg'],
  295. foreground=theme['accent'],
  296. font=('Consolas', 10, 'bold')
  297. )
  298.  
  299. # Фон главного окна
  300. self.root.configure(bg=theme['bg'])
  301.  
  302. return theme
  303.  
  304.  
  305. class IconGenerator:
  306. """Генератор иконки в стиле Пикабу."""
  307.  
  308. @staticmethod
  309. def create_pikabu_icon(size: int = 64) -> Image.Image:
  310. """Создаёт иконку в стиле Пикабу (зелёный монстрик)."""
  311. img = Image.new('RGBA', (size, size), (0, 0, 0, 0))
  312. draw = ImageDraw.Draw(img)
  313.  
  314. green = '#7bc143'
  315. dark_green = '#5a9e2f'
  316. white = '#ffffff'
  317.  
  318. padding = size // 8
  319.  
  320. # Тело
  321. draw.ellipse(
  322. [padding, padding, size - padding, size - padding],
  323. fill=green,
  324. outline=dark_green,
  325. width=max(1, size // 32)
  326. )
  327.  
  328. # Глаза
  329. eye_size = size // 5
  330. eye_y = size // 3
  331. left_eye_x = size // 3 - eye_size // 2
  332. right_eye_x = 2 * size // 3 - eye_size // 2
  333.  
  334. draw.ellipse(
  335. [left_eye_x, eye_y, left_eye_x + eye_size, eye_y + eye_size],
  336. fill=white
  337. )
  338. draw.ellipse(
  339. [right_eye_x, eye_y, right_eye_x + eye_size, eye_y + eye_size],
  340. fill=white
  341. )
  342.  
  343. # Зрачки
  344. pupil_size = eye_size // 2
  345. pupil_offset = eye_size // 4
  346. draw.ellipse(
  347. [left_eye_x + pupil_offset, eye_y + pupil_offset,
  348. left_eye_x + pupil_offset + pupil_size, eye_y + pupil_offset + pupil_size],
  349. fill='#333333'
  350. )
  351. draw.ellipse(
  352. [right_eye_x + pupil_offset, eye_y + pupil_offset,
  353. right_eye_x + pupil_offset + pupil_size, eye_y + pupil_offset + pupil_size],
  354. fill='#333333'
  355. )
  356.  
  357. # Улыбка
  358. smile_y = size // 2 + size // 8
  359. smile_width = size // 3
  360. smile_x = size // 2 - smile_width // 2
  361. draw.arc(
  362. [smile_x, smile_y, smile_x + smile_width, smile_y + size // 4],
  363. start=0, end=180,
  364. fill=dark_green,
  365. width=max(2, size // 16)
  366. )
  367.  
  368. # Рожки
  369. antenna_y = padding + size // 16
  370. draw.ellipse(
  371. [size // 3 - size // 16, antenna_y - size // 8,
  372. size // 3 + size // 16, antenna_y + size // 16],
  373. fill=green, outline=dark_green
  374. )
  375. draw.ellipse(
  376. [2 * size // 3 - size // 16, antenna_y - size // 8,
  377. 2 * size // 3 + size // 16, antenna_y + size // 16],
  378. fill=green, outline=dark_green
  379. )
  380.  
  381. return img
  382.  
  383. @staticmethod
  384. def set_app_icon(root: tk.Tk) -> None:
  385. """Устанавливает иконку приложения."""
  386. try:
  387. icon_sizes = [16, 32, 48, 64, 128, 256]
  388. icons = []
  389.  
  390. for size in icon_sizes:
  391. icon = IconGenerator.create_pikabu_icon(size)
  392. icons.append(ImageTk.PhotoImage(icon))
  393.  
  394. root._icons = icons
  395. root.iconphoto(True, *icons)
  396.  
  397. except Exception as e:
  398. print(f"Не удалось установить иконку: {e}")
  399.  
  400.  
  401. class PikabuAvatarMaker:
  402. """Приложение для создания аватарки и фона профиля Pikabu."""
  403.  
  404. # Параметры Пикабу
  405. EXACT_BG_WIDTH = 700
  406. EXACT_BG_HEIGHT = 140
  407. EXACT_AVATAR_SIZE = 128
  408. EXACT_AVATAR_LEFT = 24
  409. EXACT_AVATAR_TOP = 50
  410. EXACT_BORDER_SIZE = 3
  411.  
  412. # Параметры окна
  413. MIN_WINDOW_WIDTH = 600
  414. MIN_WINDOW_HEIGHT = 600
  415.  
  416. # Валидация
  417. MIN_INPUT_WIDTH = 100
  418. MIN_INPUT_HEIGHT = 28
  419. INVALID_FILENAME_CHARS = r'[<>:"/\\|?*]'
  420.  
  421. # Информация
  422. AUTHOR_NAME = "KOTOFEICH"
  423. AUTHOR_URL = "https://pikabu.ru/@K0T0FEICH"
  424. APP_TITLE = "Pikabu Avatar Maker"
  425.  
  426. def __init__(self, root: tk.Tk):
  427. self.root = root
  428. self.root.title(self.APP_TITLE)
  429. self.root.geometry("1000x850")
  430. self.root.minsize(self.MIN_WINDOW_WIDTH, self.MIN_WINDOW_HEIGHT)
  431.  
  432. IconGenerator.set_app_icon(root)
  433.  
  434. self.theme_manager = ThemeManager(root)
  435.  
  436. # Переменные
  437. self.input_path = tk.StringVar()
  438. self.output_prefix = tk.StringVar(value="pikabu")
  439. self.include_border = tk.BooleanVar(value=True)
  440. self.status_text = tk.StringVar(value="Готово к работе")
  441.  
  442. # Смещение по вертикали (0 = по центру)
  443. self.vertical_offset = tk.DoubleVar(value=0.0)
  444. self.offset_limits = {'min': 0, 'max': 0}
  445.  
  446. # Данные
  447. self.original_image: Image.Image | None = None
  448. self._tk_images: dict[str, ImageTk.PhotoImage] = {}
  449. self._resize_job = None
  450. self._canvases: list[tk.Canvas] = []
  451. self._entries: list[ttk.Entry] = []
  452.  
  453. self._create_widgets()
  454. self._setup_resize_handler()
  455. self._apply_current_theme()
  456.  
  457. def _create_widgets(self) -> None:
  458. """Создание всех виджетов."""
  459. self.main = ttk.Frame(self.root, padding=15)
  460. self.main.grid(row=0, column=0, sticky="nsew")
  461.  
  462. self.root.columnconfigure(0, weight=1)
  463. self.root.rowconfigure(0, weight=1)
  464.  
  465. self.main.columnconfigure(0, weight=1)
  466. self.main.rowconfigure(5, weight=1)
  467.  
  468. self._create_header(row=0)
  469. self._create_file_section(row=1)
  470. self._create_settings_section(row=2)
  471. self._create_theme_section(row=3)
  472. self._create_offset_section(row=4)
  473. self._create_preview_section(row=5)
  474.  
  475. self.debug_label = ttk.Label(self.main, text="", style='Debug.TLabel')
  476. self.debug_label.grid(row=6, column=0, sticky="ew", pady=5)
  477.  
  478. self._create_buttons_section(row=7)
  479. self._create_footer(row=8)
  480.  
  481. def _create_header(self, row: int) -> None:
  482. """Заголовок приложения."""
  483. header = ttk.Frame(self.main)
  484. header.grid(row=row, column=0, sticky="ew", pady=(0, 10))
  485. header.columnconfigure(0, weight=1)
  486.  
  487. title_frame = ttk.Frame(header)
  488. title_frame.grid(row=0, column=0)
  489.  
  490. icon_small = IconGenerator.create_pikabu_icon(32)
  491. self._tk_images['header_icon'] = ImageTk.PhotoImage(icon_small)
  492.  
  493. icon_label = ttk.Label(title_frame, image=self._tk_images['header_icon'])
  494. icon_label.pack(side="left", padx=(0, 10))
  495.  
  496. ttk.Label(
  497. title_frame,
  498. text=self.APP_TITLE,
  499. style='Title.TLabel'
  500. ).pack(side="left")
  501.  
  502. def _create_file_section(self, row: int) -> None:
  503. """Секция выбора файла."""
  504. lf = ttk.LabelFrame(self.main, text="📁 Исходное изображение", padding=10)
  505. lf.grid(row=row, column=0, sticky="ew", pady=5)
  506. lf.columnconfigure(1, weight=1)
  507.  
  508. ttk.Label(lf, text="Файл:").grid(row=0, column=0, sticky="w")
  509.  
  510. entry = ttk.Entry(lf, textvariable=self.input_path)
  511. entry.grid(row=0, column=1, sticky="ew", padx=10)
  512. self._entries.append(entry)
  513.  
  514. ttk.Button(lf, text="Обзор...", command=self._browse_file).grid(
  515. row=0, column=2, sticky="e"
  516. )
  517.  
  518. self.file_info = ttk.Label(lf, text="Файл не выбран", foreground="gray")
  519. self.file_info.grid(row=1, column=0, columnspan=3, sticky="w", pady=(5, 0))
  520.  
  521. def _create_settings_section(self, row: int) -> None:
  522. """Секция настроек."""
  523. sf = ttk.LabelFrame(self.main, text="⚙️ Настройки", padding=10)
  524. sf.grid(row=row, column=0, sticky="ew", pady=5)
  525. sf.columnconfigure(2, weight=1)
  526.  
  527. ttk.Checkbutton(
  528. sf,
  529. text="Белая обводка аватара (3px)",
  530. variable=self.include_border,
  531. command=self._update_preview
  532. ).grid(row=0, column=0, sticky="w")
  533.  
  534. ttk.Separator(sf, orient="vertical").grid(
  535. row=0, column=1, sticky="ns", padx=20
  536. )
  537.  
  538. ttk.Label(sf, text="Префикс файлов:").grid(row=0, column=2, sticky="e")
  539.  
  540. prefix_entry = ttk.Entry(sf, textvariable=self.output_prefix, width=20)
  541. prefix_entry.grid(row=0, column=3, sticky="w", padx=(10, 0))
  542. self._entries.append(prefix_entry)
  543.  
  544. def _create_theme_section(self, row: int) -> None:
  545. """Секция переключения темы."""
  546. tf = ttk.Frame(self.main)
  547. tf.grid(row=row, column=0, sticky="ew", pady=5)
  548.  
  549. ttk.Label(tf, text="🎨 Тема:").pack(side="left")
  550.  
  551. ttk.Radiobutton(
  552. tf, text="☀️ Светлая",
  553. variable=self.theme_manager.theme_var,
  554. value=False,
  555. command=self._apply_current_theme
  556. ).pack(side="left", padx=(10, 5))
  557.  
  558. ttk.Radiobutton(
  559. tf, text="🌙 Тёмная",
  560. variable=self.theme_manager.theme_var,
  561. value=True,
  562. command=self._apply_current_theme
  563. ).pack(side="left", padx=5)
  564.  
  565. def _create_offset_section(self, row: int) -> None:
  566. """Секция вертикального смещения."""
  567. of = ttk.LabelFrame(self.main, text="↕️ Вертикальное смещение", padding=10)
  568. of.grid(row=row, column=0, sticky="ew", pady=5)
  569. of.columnconfigure(1, weight=1)
  570.  
  571. # Левая часть - кнопки вверх/вниз (фиксированная ширина)
  572. left_frame = ttk.Frame(of)
  573. left_frame.grid(row=0, column=0, sticky="w")
  574.  
  575. ttk.Button(left_frame, text="▲", width=3, command=self._offset_up).pack(side="left")
  576. ttk.Button(left_frame, text="▼", width=3, command=self._offset_down).pack(side="left", padx=(5, 0))
  577.  
  578. # Центр - слайдер (растягивается)
  579. self.offset_slider = ttk.Scale(
  580. of,
  581. from_=0,
  582. to=100,
  583. orient="horizontal",
  584. variable=self.vertical_offset,
  585. command=self._on_offset_change
  586. )
  587. self.offset_slider.grid(row=0, column=1, sticky="ew", padx=15)
  588. self.offset_slider.configure(state="disabled")
  589.  
  590. # Правая часть - кнопка сброса и лейбл (фиксированная ширина)
  591. right_frame = ttk.Frame(of)
  592. right_frame.grid(row=0, column=2, sticky="e")
  593.  
  594. ttk.Button(
  595. right_frame,
  596. text="↺ Центр",
  597. width=10,
  598. command=self._reset_offset
  599. ).pack(side="left")
  600.  
  601. # Лейбл с фиксированной шириной
  602. self.offset_label = ttk.Label(
  603. right_frame,
  604. text="Смещение: 0 px (центр)",
  605. style='Offset.TLabel',
  606. width=28,
  607. anchor="w"
  608. )
  609. self.offset_label.pack(side="left", padx=(15, 0))
  610.  
  611. def _create_preview_section(self, row: int) -> None:
  612. """Секция превью (растягиваемая)."""
  613. pf = ttk.LabelFrame(
  614. self.main,
  615. text="👁️ Предпросмотр (красный = фон, синий = аватарка)",
  616. padding=10
  617. )
  618. pf.grid(row=row, column=0, sticky="nsew", pady=5)
  619.  
  620. pf.columnconfigure(0, weight=1)
  621. pf.rowconfigure(0, weight=2)
  622. pf.rowconfigure(1, weight=1)
  623.  
  624. # Canvas для схемы
  625. schema_frame = ttk.Frame(pf)
  626. schema_frame.grid(row=0, column=0, sticky="nsew", pady=(0, 10))
  627. schema_frame.columnconfigure(0, weight=1)
  628. schema_frame.rowconfigure(0, weight=1)
  629.  
  630. self.schema_canvas = tk.Canvas(
  631. schema_frame, bg="#f5f5f5",
  632. highlightthickness=1, highlightbackground="#cccccc"
  633. )
  634. self.schema_canvas.grid(row=0, column=0, sticky="nsew")
  635. self._canvases.append(self.schema_canvas)
  636.  
  637. # Превью результата
  638. result_frame = ttk.Frame(pf)
  639. result_frame.grid(row=1, column=0, sticky="nsew")
  640. result_frame.columnconfigure(0, weight=3)
  641. result_frame.columnconfigure(1, weight=1)
  642. result_frame.rowconfigure(0, weight=1)
  643.  
  644. # Фон
  645. bg_frame = ttk.LabelFrame(result_frame, text="Фон (700×140)", padding=5)
  646. bg_frame.grid(row=0, column=0, sticky="nsew", padx=(0, 10))
  647. bg_frame.columnconfigure(0, weight=1)
  648. bg_frame.rowconfigure(0, weight=1)
  649.  
  650. self.bg_canvas = tk.Canvas(
  651. bg_frame, bg="white",
  652. highlightthickness=1, highlightbackground="#cccccc"
  653. )
  654. self.bg_canvas.grid(row=0, column=0, sticky="nsew")
  655. self._canvases.append(self.bg_canvas)
  656.  
  657. # Аватар
  658. avatar_frame = ttk.LabelFrame(result_frame, text="Аватар (128×128)", padding=5)
  659. avatar_frame.grid(row=0, column=1, sticky="nsew")
  660. avatar_frame.columnconfigure(0, weight=1)
  661. avatar_frame.rowconfigure(0, weight=1)
  662.  
  663. self.avatar_canvas = tk.Canvas(
  664. avatar_frame, bg="#e0e0e0",
  665. highlightthickness=1, highlightbackground="#cccccc"
  666. )
  667. self.avatar_canvas.grid(row=0, column=0, sticky="nsew")
  668. self._canvases.append(self.avatar_canvas)
  669.  
  670. def _create_buttons_section(self, row: int) -> None:
  671. """Секция кнопок."""
  672. bf = ttk.Frame(self.main)
  673. bf.grid(row=row, column=0, pady=10, sticky="ew")
  674. bf.columnconfigure(0, weight=1)
  675. bf.columnconfigure(2, weight=1)
  676.  
  677. self.btn_create = ttk.Button(
  678. bf,
  679. text="💾 Создать и сохранить",
  680. style='Accent.TButton',
  681. state="disabled",
  682. command=self._create_exact_set
  683. )
  684. self.btn_create.grid(row=0, column=1)
  685.  
  686. self.status_label = ttk.Label(bf, textvariable=self.status_text)
  687. self.status_label.grid(row=0, column=2, sticky="e", padx=(20, 0))
  688.  
  689. def _create_footer(self, row: int) -> None:
  690. """Футер с информацией об авторе."""
  691. footer = ttk.Frame(self.main)
  692. footer.grid(row=row, column=0, sticky="ew", pady=(10, 0))
  693. footer.columnconfigure(0, weight=1)
  694.  
  695. ttk.Separator(footer, orient="horizontal").grid(
  696. row=0, column=0, sticky="ew", pady=(0, 10)
  697. )
  698.  
  699. link_frame = ttk.Frame(footer)
  700. link_frame.grid(row=1, column=0)
  701.  
  702. ttk.Label(link_frame, text="by").pack(side="left")
  703.  
  704. self.author_link = ttk.Label(
  705. link_frame,
  706. text=self.AUTHOR_NAME,
  707. style='Link.TLabel',
  708. cursor="hand2"
  709. )
  710. self.author_link.pack(side="left", padx=5)
  711. self.author_link.bind("<Button-1>", self._open_author_page)
  712. self.author_link.bind("<Enter>", self._on_link_hover)
  713. self.author_link.bind("<Leave>", self._on_link_leave)
  714.  
  715. ttk.Label(link_frame, text="• pikabu.ru").pack(side="left")
  716.  
  717. # =========================================================================
  718. # Управление смещением
  719. # =========================================================================
  720.  
  721. def _calculate_offset_limits(self) -> None:
  722. """Вычисляет допустимые границы смещения."""
  723. if not self.original_image:
  724. self.offset_limits = {'min': 0, 'max': 0}
  725. return
  726.  
  727. img = self.original_image
  728. w, h = img.size
  729.  
  730. target_ratio = self.EXACT_BG_WIDTH / self.EXACT_BG_HEIGHT
  731. current_ratio = w / h
  732.  
  733. if current_ratio > target_ratio:
  734. # Широкое изображение - нет вертикального смещения
  735. self.offset_limits = {'min': 0, 'max': 0}
  736. else:
  737. # Высокое изображение - можно двигать вертикально
  738. crop_w = w
  739. crop_h = round(w / target_ratio)
  740. scale = crop_w / self.EXACT_BG_WIDTH
  741.  
  742. avatar_size_src = round(self.EXACT_AVATAR_SIZE * scale)
  743. avatar_offset_y = round(self.EXACT_AVATAR_TOP * scale)
  744.  
  745. center_bg_top = (h - crop_h) // 2
  746.  
  747. # Максимум вверх: фон не выходит за верхний край
  748. max_up = center_bg_top
  749.  
  750. # Максимум вниз: аватарка не выходит за нижний край
  751. max_bg_top_for_avatar = h - avatar_offset_y - avatar_size_src
  752. max_down = max_bg_top_for_avatar - center_bg_top
  753.  
  754. # Также проверяем, чтобы фон не выходил за нижний край
  755. max_bg_top_for_bg = h - crop_h
  756. max_down_for_bg = max_bg_top_for_bg - center_bg_top
  757.  
  758. max_down = min(max_down, max_down_for_bg)
  759.  
  760. self.offset_limits = {
  761. 'min': -max_up,
  762. 'max': max(0, max_down)
  763. }
  764.  
  765. self._update_slider_range()
  766.  
  767. def _update_slider_range(self) -> None:
  768. """Обновляет диапазон слайдера."""
  769. min_val = self.offset_limits['min']
  770. max_val = self.offset_limits['max']
  771.  
  772. if min_val == max_val == 0:
  773. self.offset_slider.configure(state="disabled")
  774. self.vertical_offset.set(0)
  775. else:
  776. self.offset_slider.configure(
  777. state="normal",
  778. from_=min_val,
  779. to=max_val
  780. )
  781. current = self.vertical_offset.get()
  782. if current < min_val:
  783. self.vertical_offset.set(min_val)
  784. elif current > max_val:
  785. self.vertical_offset.set(max_val)
  786.  
  787. self._update_offset_label()
  788.  
  789. def _on_offset_change(self, value=None) -> None:
  790. """Обработчик изменения слайдера."""
  791. self._update_offset_label()
  792. self._update_preview()
  793.  
  794. def _offset_up(self) -> None:
  795. """Сместить вверх на 10 пикселей."""
  796. if not self.original_image:
  797. return
  798. current = self.vertical_offset.get()
  799. new_val = max(self.offset_limits['min'], current - 10)
  800. self.vertical_offset.set(new_val)
  801. self._on_offset_change()
  802.  
  803. def _offset_down(self) -> None:
  804. """Сместить вниз на 10 пикселей."""
  805. if not self.original_image:
  806. return
  807. current = self.vertical_offset.get()
  808. new_val = min(self.offset_limits['max'], current + 10)
  809. self.vertical_offset.set(new_val)
  810. self._on_offset_change()
  811.  
  812. def _reset_offset(self) -> None:
  813. """Сбросить смещение в центр."""
  814. self.vertical_offset.set(0)
  815. self._on_offset_change()
  816.  
  817. def _update_offset_label(self) -> None:
  818. """Обновить отображение текущего смещения."""
  819. offset = int(self.vertical_offset.get())
  820.  
  821. if offset < 0:
  822. text = f"Смещение: {offset:>5} px (вверх)"
  823. elif offset > 0:
  824. text = f"Смещение: +{offset:<4} px (вниз)"
  825. else:
  826. text = f"Смещение: 0 px (центр)"
  827.  
  828. self.offset_label.config(text=text)
  829.  
  830. # =========================================================================
  831. # Hover и ссылки
  832. # =========================================================================
  833.  
  834. def _on_link_hover(self, event=None) -> None:
  835. """Hover эффект для ссылки."""
  836. self.author_link.configure(font=('Arial', 10, 'underline bold'))
  837.  
  838. def _on_link_leave(self, event=None) -> None:
  839. """Убираем hover эффект."""
  840. self.author_link.configure(font=('Arial', 10, 'underline'))
  841.  
  842. def _open_author_page(self, event=None) -> None:
  843. """Открывает страницу автора в браузере."""
  844. try:
  845. webbrowser.open(self.AUTHOR_URL)
  846. except Exception as e:
  847. messagebox.showerror("Ошибка", f"Не удалось открыть ссылку:\n{e}")
  848.  
  849. def _apply_current_theme(self) -> None:
  850. """Применяет текущую выбранную тему."""
  851. theme = self.theme_manager.apply_theme(self.theme_manager.is_dark)
  852.  
  853. for canvas in self._canvases:
  854. canvas.configure(
  855. bg=theme['canvas_bg'],
  856. highlightbackground=theme['border']
  857. )
  858.  
  859. if self.original_image:
  860. self.file_info.config(foreground=theme['accent'])
  861. else:
  862. self.file_info.config(foreground=theme['disabled_fg'])
  863.  
  864. self._update_preview()
  865.  
  866. def _setup_resize_handler(self) -> None:
  867. """Настройка обработчика изменения размера."""
  868. for canvas in self._canvases:
  869. canvas.bind("<Configure>", self._on_canvas_resize)
  870.  
  871. def _on_canvas_resize(self, event=None) -> None:
  872. """Обработчик изменения размера с debounce."""
  873. if self._resize_job:
  874. self.root.after_cancel(self._resize_job)
  875. self._resize_job = self.root.after(100, self._update_preview)
  876.  
  877. # =========================================================================
  878. # Работа с файлами
  879. # =========================================================================
  880.  
  881. def _browse_file(self) -> None:
  882. """Открытие диалога выбора файла."""
  883. filename = filedialog.askopenfilename(
  884. title="Выберите изображение",
  885. filetypes=[
  886. ("Изображения", "*.png *.jpg *.jpeg *.webp *.bmp *.gif"),
  887. ("Все файлы", "*.*")
  888. ]
  889. )
  890. if filename:
  891. self._load_image(filename)
  892.  
  893. def _load_image(self, path: str) -> None:
  894. """Загрузка и валидация изображения."""
  895. try:
  896. with Image.open(path) as img:
  897. converted = self._normalize_image_mode(img)
  898. self.original_image = converted.copy()
  899.  
  900. self._validate_image_size(self.original_image)
  901.  
  902. self.input_path.set(path)
  903. theme = self.theme_manager.current_theme
  904. self.file_info.config(
  905. text=f"✓ {os.path.basename(path)} — {self.original_image.width}×{self.original_image.height} px",
  906. foreground=theme['accent']
  907. )
  908. self.btn_create.config(state="normal")
  909. self.status_text.set("Изображение загружено")
  910.  
  911. # Сбрасываем смещение и вычисляем новые границы
  912. self.vertical_offset.set(0)
  913. self._calculate_offset_limits()
  914.  
  915. self._update_preview()
  916.  
  917. except Exception as e:
  918. messagebox.showerror("Ошибка загрузки", str(e))
  919. self.original_image = None
  920.  
  921. def _validate_image_size(self, img: Image.Image) -> bool:
  922. """Проверка минимального размера."""
  923. if img.width < self.MIN_INPUT_WIDTH or img.height < self.MIN_INPUT_HEIGHT:
  924. messagebox.showwarning(
  925. "Предупреждение",
  926. f"Изображение маленькое ({img.width}×{img.height}).\n"
  927. f"Рекомендуемый минимум: {self.MIN_INPUT_WIDTH}×{self.MIN_INPUT_HEIGHT} px."
  928. )
  929. return True
  930.  
  931. def _sanitize_filename(self, prefix: str) -> str:
  932. """Очистка префикса."""
  933. sanitized = re.sub(self.INVALID_FILENAME_CHARS, '_', prefix.strip())
  934. return sanitized if sanitized else "pikabu"
  935.  
  936. # =========================================================================
  937. # Обработка изображений
  938. # =========================================================================
  939.  
  940. @staticmethod
  941. def _normalize_image_mode(img: Image.Image) -> Image.Image:
  942. """Приведение к RGB/RGBA."""
  943. if img.mode == 'P':
  944. return img.convert('RGBA')
  945. elif img.mode not in ('RGB', 'RGBA'):
  946. return img.convert('RGB')
  947. return img.copy()
  948.  
  949. @staticmethod
  950. def _to_rgb_with_white_bg(img: Image.Image) -> Image.Image:
  951. """Конвертация в RGB."""
  952. if img.mode == 'RGBA':
  953. rgb = Image.new('RGB', img.size, (255, 255, 255))
  954. rgb.paste(img, mask=img.split()[3])
  955. return rgb
  956. elif img.mode != 'RGB':
  957. return img.convert('RGB')
  958. return img
  959.  
  960. def _calculate_regions(self, image: Image.Image) -> tuple[tuple, tuple, float]:
  961. """Вычисляет области фона и аватарки с учётом смещения."""
  962. w, h = image.size
  963.  
  964. if h == 0 or w == 0:
  965. raise ValueError("Изображение имеет нулевой размер")
  966.  
  967. target_ratio = self.EXACT_BG_WIDTH / self.EXACT_BG_HEIGHT
  968. current_ratio = w / h
  969.  
  970. offset = int(self.vertical_offset.get())
  971.  
  972. if current_ratio > target_ratio:
  973. # Широкое изображение - обрезаем бока
  974. crop_h = h
  975. crop_w = round(h * target_ratio)
  976. bg_left = (w - crop_w) // 2
  977. bg_top = 0
  978. else:
  979. # Высокое изображение - обрезаем верх/низ с учётом смещения
  980. crop_w = w
  981. crop_h = round(w / target_ratio)
  982. bg_left = 0
  983. center_top = (h - crop_h) // 2
  984. bg_top = center_top + offset
  985.  
  986. # Ограничиваем границами
  987. bg_top = max(0, min(bg_top, h - crop_h))
  988.  
  989. bg_box = (bg_left, bg_top, bg_left + crop_w, bg_top + crop_h)
  990. scale = crop_w / self.EXACT_BG_WIDTH
  991.  
  992. avatar_offset_x = round(self.EXACT_AVATAR_LEFT * scale)
  993. avatar_offset_y = round(self.EXACT_AVATAR_TOP * scale)
  994. avatar_size_src = round(self.EXACT_AVATAR_SIZE * scale)
  995.  
  996. avatar_left = bg_left + avatar_offset_x
  997. avatar_top = bg_top + avatar_offset_y
  998.  
  999. avatar_box = (
  1000. avatar_left, avatar_top,
  1001. avatar_left + avatar_size_src, avatar_top + avatar_size_src
  1002. )
  1003.  
  1004. return bg_box, avatar_box, scale
  1005.  
  1006. def _create_background(self, image: Image.Image) -> Image.Image:
  1007. """Создаёт фон 700×140."""
  1008. bg_box, _, _ = self._calculate_regions(image)
  1009. crop = image.crop(bg_box)
  1010. return crop.resize(
  1011. (self.EXACT_BG_WIDTH, self.EXACT_BG_HEIGHT),
  1012. Image.LANCZOS
  1013. )
  1014.  
  1015. def _create_avatar(self, image: Image.Image) -> Image.Image:
  1016. """Создаёт круглую аватарку 128×128."""
  1017. _, avatar_box, _ = self._calculate_regions(image)
  1018. area = self._extract_avatar_area(image, avatar_box)
  1019.  
  1020. area = area.resize(
  1021. (self.EXACT_AVATAR_SIZE, self.EXACT_AVATAR_SIZE),
  1022. Image.LANCZOS
  1023. )
  1024.  
  1025. if area.mode != 'RGBA':
  1026. area = area.convert('RGBA')
  1027.  
  1028. avatar = self._apply_circle_mask(area)
  1029.  
  1030. if self.include_border.get():
  1031. avatar = self._add_border(avatar)
  1032.  
  1033. return avatar
  1034.  
  1035. def _extract_avatar_area(
  1036. self,
  1037. image: Image.Image,
  1038. avatar_box: tuple[int, int, int, int]
  1039. ) -> Image.Image:
  1040. """Извлекает область аватарки."""
  1041. av_left, av_top, av_right, av_bottom = avatar_box
  1042. img_w, img_h = image.size
  1043.  
  1044. need_padding = (
  1045. av_left < 0 or av_top < 0 or
  1046. av_right > img_w or av_bottom > img_h
  1047. )
  1048.  
  1049. if not need_padding:
  1050. return image.crop(avatar_box)
  1051.  
  1052. pad_left = max(0, -av_left)
  1053. pad_top = max(0, -av_top)
  1054. pad_right = max(0, av_right - img_w)
  1055. pad_bottom = max(0, av_bottom - img_h)
  1056.  
  1057. new_w = img_w + pad_left + pad_right
  1058. new_h = img_h + pad_top + pad_bottom
  1059.  
  1060. bg_color = (255, 255, 255, 255) if image.mode == 'RGBA' else (255, 255, 255)
  1061. padded = Image.new(image.mode, (new_w, new_h), bg_color)
  1062. padded.paste(image, (pad_left, pad_top))
  1063.  
  1064. adjusted_box = (
  1065. av_left + pad_left, av_top + pad_top,
  1066. av_right + pad_left, av_bottom + pad_top
  1067. )
  1068.  
  1069. return padded.crop(adjusted_box)
  1070.  
  1071. def _apply_circle_mask(self, image: Image.Image) -> Image.Image:
  1072. """Применяет круглую маску."""
  1073. size = image.size[0]
  1074. mask = Image.new("L", (size, size), 0)
  1075. ImageDraw.Draw(mask).ellipse((0, 0, size, size), fill=255)
  1076.  
  1077. result = Image.new("RGBA", (size, size), (0, 0, 0, 0))
  1078. result.paste(image, (0, 0))
  1079. result.putalpha(mask)
  1080.  
  1081. return result
  1082.  
  1083. def _add_border(self, avatar: Image.Image) -> Image.Image:
  1084. """Добавляет белую обводку."""
  1085. b = self.EXACT_BORDER_SIZE
  1086. s = avatar.size[0] + b * 2
  1087.  
  1088. result = Image.new("RGBA", (s, s), (0, 0, 0, 0))
  1089. draw = ImageDraw.Draw(result)
  1090. draw.ellipse((0, 0, s, s), fill=(255, 255, 255, 255))
  1091. result.paste(avatar, (b, b), avatar)
  1092.  
  1093. return result
  1094.  
  1095. # =========================================================================
  1096. # Превью
  1097. # =========================================================================
  1098.  
  1099. def _update_preview(self) -> None:
  1100. """Обновление превью."""
  1101. if not self.original_image:
  1102. self._draw_empty_canvases()
  1103. return
  1104.  
  1105. try:
  1106. self._update_debug_info()
  1107. self._draw_schema()
  1108. self._update_result_previews()
  1109. except Exception as e:
  1110. self.debug_label.config(text=f"Ошибка: {e}")
  1111.  
  1112. def _draw_empty_canvases(self) -> None:
  1113. """Пустые canvas'ы."""
  1114. theme = self.theme_manager.current_theme
  1115.  
  1116. for canvas in self._canvases:
  1117. canvas.delete("all")
  1118.  
  1119. cw = self.schema_canvas.winfo_width()
  1120. ch = self.schema_canvas.winfo_height()
  1121. if cw > 1 and ch > 1:
  1122. self.schema_canvas.create_text(
  1123. cw // 2, ch // 2,
  1124. text="Выберите изображение для предпросмотра",
  1125. fill=theme['debug_fg'], font=("Arial", 12)
  1126. )
  1127.  
  1128. def _update_debug_info(self) -> None:
  1129. """Обновление debug-информации."""
  1130. img = self.original_image
  1131. bg_box, avatar_box, scale = self._calculate_regions(img)
  1132. offset = int(self.vertical_offset.get())
  1133.  
  1134. debug = f"Масштаб: {scale:.2f}x | Смещение: {offset}px | "
  1135. debug += f"Фон Y: [{bg_box[1]}→{bg_box[3]}] | "
  1136. debug += f"Аватар Y: [{avatar_box[1]}→{avatar_box[3]}]"
  1137.  
  1138. if avatar_box[3] > img.height:
  1139. debug += f" | ⚠️ Аватар за границей!"
  1140.  
  1141. self.debug_label.config(text=debug)
  1142.  
  1143. def _draw_schema(self) -> None:
  1144. """Отрисовка схемы."""
  1145. img = self.original_image
  1146. bg_box, avatar_box, _ = self._calculate_regions(img)
  1147. theme = self.theme_manager.current_theme
  1148.  
  1149. self.schema_canvas.delete("all")
  1150.  
  1151. canvas_w = self.schema_canvas.winfo_width()
  1152. canvas_h = self.schema_canvas.winfo_height()
  1153.  
  1154. if canvas_w < 10 or canvas_h < 10:
  1155. return
  1156.  
  1157. padding = 15
  1158. available_w = canvas_w - padding * 2
  1159. available_h = canvas_h - padding * 2
  1160.  
  1161. display_scale = min(available_w / img.width, available_h / img.height)
  1162. dw = int(img.width * display_scale)
  1163. dh = int(img.height * display_scale)
  1164. offset_x = (canvas_w - dw) // 2
  1165. offset_y = (canvas_h - dh) // 2
  1166.  
  1167. # Миниатюра
  1168. thumb = img.copy()
  1169. thumb.thumbnail((dw, dh), Image.LANCZOS)
  1170. self._tk_images['thumb'] = ImageTk.PhotoImage(thumb)
  1171. self.schema_canvas.create_image(
  1172. offset_x, offset_y, anchor="nw", image=self._tk_images['thumb']
  1173. )
  1174.  
  1175. # Рамка исходника
  1176. self.schema_canvas.create_rectangle(
  1177. offset_x, offset_y, offset_x + dw, offset_y + dh,
  1178. outline=theme['border'], width=1
  1179. )
  1180.  
  1181. # Рамка фона (красная)
  1182. bx1 = offset_x + int(bg_box[0] * display_scale)
  1183. by1 = offset_y + int(bg_box[1] * display_scale)
  1184. bx2 = offset_x + int(bg_box[2] * display_scale)
  1185. by2 = offset_y + int(bg_box[3] * display_scale)
  1186. self.schema_canvas.create_rectangle(
  1187. bx1, by1, bx2, by2, outline="#e74c3c", width=2
  1188. )
  1189. self.schema_canvas.create_text(
  1190. bx1 + 8, by1 + 8, anchor="nw",
  1191. text="Фон", fill="#e74c3c", font=("Arial", 10, "bold")
  1192. )
  1193.  
  1194. # Рамка аватарки (синяя)
  1195. ax1 = offset_x + int(avatar_box[0] * display_scale)
  1196. ay1 = offset_y + int(avatar_box[1] * display_scale)
  1197. ax2 = offset_x + int(avatar_box[2] * display_scale)
  1198. ay2 = offset_y + int(avatar_box[3] * display_scale)
  1199. self.schema_canvas.create_oval(
  1200. ax1, ay1, ax2, ay2, outline="#3498db", width=2
  1201. )
  1202. self.schema_canvas.create_text(
  1203. (ax1 + ax2) // 2, (ay1 + ay2) // 2,
  1204. text="Аватар", fill="#3498db", font=("Arial", 10, "bold")
  1205. )
  1206.  
  1207. def _update_result_previews(self) -> None:
  1208. """Обновление превью результатов."""
  1209. img = self.original_image
  1210.  
  1211. # === Фон ===
  1212. bg = self._create_background(img)
  1213.  
  1214. bg_canvas_w = self.bg_canvas.winfo_width()
  1215. bg_canvas_h = self.bg_canvas.winfo_height()
  1216.  
  1217. if bg_canvas_w > 10 and bg_canvas_h > 10:
  1218. bg_scale = min(
  1219. (bg_canvas_w - 10) / bg.width,
  1220. (bg_canvas_h - 10) / bg.height
  1221. )
  1222. new_w = max(1, int(bg.width * bg_scale))
  1223. new_h = max(1, int(bg.height * bg_scale))
  1224.  
  1225. bg_preview = bg.resize((new_w, new_h), Image.LANCZOS)
  1226. self._tk_images['bg'] = ImageTk.PhotoImage(bg_preview)
  1227.  
  1228. self.bg_canvas.delete("all")
  1229. self.bg_canvas.create_image(
  1230. bg_canvas_w // 2, bg_canvas_h // 2,
  1231. anchor="center", image=self._tk_images['bg']
  1232. )
  1233.  
  1234. # === Аватар ===
  1235. avatar = self._create_avatar(img)
  1236.  
  1237. av_canvas_w = self.avatar_canvas.winfo_width()
  1238. av_canvas_h = self.avatar_canvas.winfo_height()
  1239.  
  1240. if av_canvas_w > 10 and av_canvas_h > 10:
  1241. av_scale = min(
  1242. (av_canvas_w - 10) / avatar.width,
  1243. (av_canvas_h - 10) / avatar.height,
  1244. 1.5
  1245. )
  1246. new_size = max(1, int(avatar.width * av_scale))
  1247.  
  1248. avatar_preview = avatar.resize((new_size, new_size), Image.LANCZOS)
  1249. self._tk_images['avatar'] = ImageTk.PhotoImage(avatar_preview)
  1250.  
  1251. self.avatar_canvas.delete("all")
  1252. self.avatar_canvas.create_image(
  1253. av_canvas_w // 2, av_canvas_h // 2,
  1254. anchor="center", image=self._tk_images['avatar']
  1255. )
  1256.  
  1257. # =========================================================================
  1258. # Сохранение
  1259. # =========================================================================
  1260.  
  1261. def _create_exact_set(self) -> None:
  1262. """Создание и сохранение набора."""
  1263. if not self.original_image:
  1264. messagebox.showwarning("Предупреждение", "Сначала выберите изображение")
  1265. return
  1266.  
  1267. try:
  1268. out_dir = os.path.dirname(self.input_path.get()) or "."
  1269. prefix = self._sanitize_filename(self.output_prefix.get())
  1270.  
  1271. bg = self._create_background(self.original_image)
  1272. avatar = self._create_avatar(self.original_image)
  1273.  
  1274. bg_path = os.path.join(out_dir, f"{prefix}_background_700x140.jpg")
  1275. av_path = os.path.join(out_dir, f"{prefix}_avatar_128.png")
  1276.  
  1277. bg_rgb = self._to_rgb_with_white_bg(bg)
  1278. bg_rgb.save(bg_path, "JPEG", quality=95, subsampling=0)
  1279. avatar.save(av_path, "PNG")
  1280.  
  1281. self.status_text.set("✅ Файлы сохранены!")
  1282. messagebox.showinfo(
  1283. "Готово!",
  1284. f"Файлы успешно созданы:\n\n"
  1285. f"📷 {os.path.basename(bg_path)}\n"
  1286. f"👤 {os.path.basename(av_path)}\n\n"
  1287. f"📁 Папка: {out_dir}"
  1288. )
  1289.  
  1290. except PermissionError:
  1291. messagebox.showerror("Ошибка", "Нет прав на запись в выбранную папку")
  1292. except Exception as e:
  1293. import traceback
  1294. messagebox.showerror("Ошибка", f"{e}\n\n{traceback.format_exc()}")
  1295.  
  1296.  
  1297. def main():
  1298. """Точка входа."""
  1299. # Проверка на повторный запуск
  1300. instance = SingleInstance("PikabuAvatarMaker")
  1301.  
  1302. if instance.is_running():
  1303. # Показываем сообщение без создания полного окна
  1304. root = tk.Tk()
  1305. root.withdraw() # Скрываем главное окно
  1306. messagebox.showwarning(
  1307. "Pikabu Avatar Maker",
  1308. "Программа уже запущена!\n\n"
  1309. "Найдите открытое окно программы на панели задач."
  1310. )
  1311. root.destroy()
  1312. sys.exit(0)
  1313.  
  1314. # Регистрируем очистку при выходе
  1315. atexit.register(instance.cleanup)
  1316.  
  1317. # Запускаем приложение
  1318. root = tk.Tk()
  1319. app = PikabuAvatarMaker(root)
  1320.  
  1321. # Очистка при закрытии окна
  1322. def on_closing():
  1323. instance.cleanup()
  1324. root.destroy()
  1325.  
  1326. root.protocol("WM_DELETE_WINDOW", on_closing)
  1327. root.mainloop()
  1328.  
  1329.  
  1330. if __name__ == "__main__":
  1331. main()
Advertisement
Add Comment
Please, Sign In to add comment