thumbnail-maker 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,299 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 이벤트 핸들러 모듈
5
+ """
6
+
7
+ import os
8
+ import json
9
+ import tempfile
10
+ import base64
11
+ from PySide6.QtWidgets import (QColorDialog, QFileDialog, QMessageBox,
12
+ QDialog, QVBoxLayout, QPlainTextEdit, QDialogButtonBox)
13
+ from PySide6.QtGui import QColor, QPixmap
14
+ from PySide6.QtCore import Qt
15
+
16
+ from ..renderer import ThumbnailRenderer
17
+ from .font_utils import infer_font_name_from_file
18
+
19
+
20
+ class EventHandlers:
21
+ """이벤트 핸들러 클래스"""
22
+
23
+ @staticmethod
24
+ def on_resolution_mode_changed(gui, mode):
25
+ """해상도 모드 변경 시 처리"""
26
+ if mode == 'preset':
27
+ gui.aspect_ratio.setEnabled(True)
28
+ elif mode == 'fixedRatio':
29
+ gui.aspect_ratio.setEnabled(True)
30
+ # fixedRatio 모드로 변경 시 현재 너비를 기준으로 높이 재계산
31
+ EventHandlers.on_aspect_ratio_changed(gui, gui.aspect_ratio.currentText())
32
+ else: # custom
33
+ gui.aspect_ratio.setEnabled(False)
34
+ gui.update_preview()
35
+
36
+ @staticmethod
37
+ def on_aspect_ratio_changed(gui, ratio):
38
+ """비율 변경 시 처리"""
39
+ # fixedRatio 모드일 때만 너비를 기준으로 높이 재계산
40
+ if gui.res_mode.currentText() == 'fixedRatio':
41
+ try:
42
+ parts = ratio.split(':')
43
+ if len(parts) == 2:
44
+ rw, rh = float(parts[0]), float(parts[1])
45
+ if rw > 0 and rh > 0:
46
+ aspect_ratio = rw / rh
47
+ current_width = gui.width_spin.value()
48
+ new_height = int(current_width / aspect_ratio)
49
+ if not gui._updating_dimension:
50
+ gui._updating_dimension = True
51
+ gui.height_spin.setValue(new_height)
52
+ gui._updating_dimension = False
53
+ except (ValueError, ZeroDivisionError):
54
+ pass
55
+
56
+ gui.update_preview()
57
+
58
+ @staticmethod
59
+ def on_width_changed(gui, value):
60
+ """너비 변경 시 처리"""
61
+ if gui._updating_dimension:
62
+ return
63
+
64
+ # fixedRatio 모드일 때 비율에 맞게 높이 조정
65
+ if gui.res_mode.currentText() == 'fixedRatio':
66
+ ratio_str = gui.aspect_ratio.currentText()
67
+ try:
68
+ parts = ratio_str.split(':')
69
+ if len(parts) == 2:
70
+ rw, rh = float(parts[0]), float(parts[1])
71
+ if rw > 0 and rh > 0:
72
+ aspect_ratio = rw / rh
73
+ new_height = int(value / aspect_ratio)
74
+ gui._updating_dimension = True
75
+ gui.height_spin.setValue(new_height)
76
+ gui._updating_dimension = False
77
+ except (ValueError, ZeroDivisionError):
78
+ pass
79
+
80
+ gui.update_preview()
81
+
82
+ @staticmethod
83
+ def on_height_changed(gui, value):
84
+ """높이 변경 시 처리"""
85
+ if gui._updating_dimension:
86
+ return
87
+
88
+ # fixedRatio 모드일 때 비율에 맞게 너비 조정
89
+ if gui.res_mode.currentText() == 'fixedRatio':
90
+ ratio_str = gui.aspect_ratio.currentText()
91
+ try:
92
+ parts = ratio_str.split(':')
93
+ if len(parts) == 2:
94
+ rw, rh = float(parts[0]), float(parts[1])
95
+ if rw > 0 and rh > 0:
96
+ aspect_ratio = rw / rh
97
+ new_width = int(value * aspect_ratio)
98
+ gui._updating_dimension = True
99
+ gui.width_spin.setValue(new_width)
100
+ gui._updating_dimension = False
101
+ except (ValueError, ZeroDivisionError):
102
+ pass
103
+
104
+ gui.update_preview()
105
+
106
+ @staticmethod
107
+ def select_bg_color(gui):
108
+ """배경 색상 선택"""
109
+ color = QColorDialog.getColor(QColor(gui.bg_color))
110
+ if color.isValid():
111
+ gui.bg_color = color.name()
112
+ gui.update_preview()
113
+
114
+ @staticmethod
115
+ def select_title_color(gui):
116
+ """제목 색상 선택"""
117
+ color = QColorDialog.getColor(QColor(gui.title_color))
118
+ if color.isValid():
119
+ gui.title_color = color.name()
120
+ gui.update_preview()
121
+
122
+ @staticmethod
123
+ def select_subtitle_color(gui):
124
+ """부제목 색상 선택"""
125
+ color = QColorDialog.getColor(QColor(gui.subtitle_color))
126
+ if color.isValid():
127
+ gui.subtitle_color = color.name()
128
+ gui.update_preview()
129
+
130
+ @staticmethod
131
+ def select_background_image(gui):
132
+ """배경 이미지 선택"""
133
+ file_path, _ = QFileDialog.getOpenFileName(
134
+ gui, '배경 이미지 선택', '', 'Images (*.png *.jpg *.jpeg *.gif *.bmp)'
135
+ )
136
+ if file_path:
137
+ gui.bg_image_path.setText(file_path)
138
+ gui.update_preview()
139
+
140
+ @staticmethod
141
+ def set_title_font_name_from_path(gui, path: str):
142
+ """제목 폰트 이름을 파일 경로에서 추출하여 설정"""
143
+ inferred = infer_font_name_from_file(path)
144
+ if inferred:
145
+ gui.title_font_name.setText(inferred)
146
+ gui.update_preview()
147
+
148
+ @staticmethod
149
+ def set_subtitle_font_name_from_path(gui, path: str):
150
+ """부제목 폰트 이름을 파일 경로에서 추출하여 설정"""
151
+ inferred = infer_font_name_from_file(path)
152
+ if inferred:
153
+ gui.subtitle_font_name.setText(inferred)
154
+ gui.update_preview()
155
+
156
+ @staticmethod
157
+ def on_title_font_file_changed(gui):
158
+ """제목 폰트 파일 경로 변경 시 처리"""
159
+ path = gui.title_font_file.text().strip()
160
+ if path and not gui.title_font_name.text().strip():
161
+ EventHandlers.set_title_font_name_from_path(gui, path)
162
+ else:
163
+ gui.update_preview()
164
+
165
+ @staticmethod
166
+ def on_subtitle_font_file_changed(gui):
167
+ """부제목 폰트 파일 경로 변경 시 처리"""
168
+ path = gui.subtitle_font_file.text().strip()
169
+ if path and not gui.subtitle_font_name.text().strip():
170
+ EventHandlers.set_subtitle_font_name_from_path(gui, path)
171
+ else:
172
+ gui.update_preview()
173
+
174
+ @staticmethod
175
+ def generate_preview(gui):
176
+ """미리보기 생성"""
177
+ if not hasattr(gui, 'current_dsl'):
178
+ gui.update_preview()
179
+
180
+ gui.preview_btn.setEnabled(False)
181
+ gui.preview_btn.setText('생성 중...')
182
+
183
+ # 스레드에서 생성
184
+ from .preview_thread import PreviewThread
185
+ gui.preview_thread = PreviewThread(gui.current_dsl)
186
+ gui.preview_thread.preview_ready.connect(lambda path: EventHandlers.on_preview_ready(gui, path))
187
+ gui.preview_thread.start()
188
+
189
+ @staticmethod
190
+ def on_preview_ready(gui, file_path):
191
+ """미리보기 준비됨"""
192
+ if file_path and os.path.exists(file_path):
193
+ pixmap = QPixmap(file_path)
194
+ gui.preview_label.setPixmap(pixmap.scaled(
195
+ 480, 270, Qt.KeepAspectRatio, Qt.SmoothTransformation
196
+ ))
197
+ else:
198
+ msg = gui.preview_thread.error_message or '미리보기 생성 중 오류가 발생했습니다.'
199
+ QMessageBox.critical(gui, '에러', msg)
200
+
201
+ gui.preview_btn.setEnabled(True)
202
+ gui.preview_btn.setText('미리보기 생성')
203
+
204
+ @staticmethod
205
+ def save_thumbnail(gui):
206
+ """썸네일 저장"""
207
+ if not hasattr(gui, 'current_dsl'):
208
+ QMessageBox.warning(gui, '경고', '먼저 미리보기를 생성해주세요.')
209
+ return
210
+
211
+ file_path, _ = QFileDialog.getSaveFileName(
212
+ gui, '썸네일 저장', 'thumbnail.png', 'Images (*.png)'
213
+ )
214
+
215
+ if file_path:
216
+ try:
217
+ ThumbnailRenderer.render_thumbnail(gui.current_dsl, file_path)
218
+ QMessageBox.information(gui, '완료', f'저장 완료: {file_path}')
219
+ except Exception as e:
220
+ QMessageBox.critical(gui, '에러', f'저장 실패: {e}')
221
+
222
+ @staticmethod
223
+ def show_dsl_dialog(gui):
224
+ """현재 DSL을 JSON으로 출력하는 다이얼로그"""
225
+ if not hasattr(gui, 'current_dsl'):
226
+ gui.update_preview()
227
+ from .dsl_manager import DSLManager
228
+ dsl = getattr(gui, 'current_dsl', DSLManager.generate_dsl(gui))
229
+ try:
230
+ text = json.dumps(dsl, ensure_ascii=False, indent=2)
231
+ except Exception:
232
+ text = str(dsl)
233
+
234
+ dlg = QDialog(gui)
235
+ dlg.setWindowTitle('현재 DSL 보기')
236
+ v = QVBoxLayout(dlg)
237
+ editor = QPlainTextEdit()
238
+ editor.setPlainText(text)
239
+ editor.setReadOnly(True)
240
+ v.addWidget(editor)
241
+ btns = QDialogButtonBox(QDialogButtonBox.Close)
242
+ btns.rejected.connect(dlg.reject)
243
+ btns.accepted.connect(dlg.accept)
244
+ v.addWidget(btns)
245
+ dlg.resize(700, 500)
246
+ dlg.exec()
247
+
248
+ @staticmethod
249
+ def save_dsl(gui):
250
+ """현재 DSL을 JSON 파일로 저장"""
251
+ if not hasattr(gui, 'current_dsl'):
252
+ gui.update_preview()
253
+ from .dsl_manager import DSLManager
254
+ dsl = getattr(gui, 'current_dsl', DSLManager.generate_dsl(gui))
255
+
256
+ file_path, _ = QFileDialog.getSaveFileName(
257
+ gui, 'DSL 저장', 'thumbnail.json', 'JSON (*.json)'
258
+ )
259
+ if not file_path:
260
+ return
261
+ try:
262
+ with open(file_path, 'w', encoding='utf-8') as f:
263
+ json.dump(dsl, f, ensure_ascii=False, indent=2)
264
+ QMessageBox.information(gui, '완료', f'DSL 저장 완료: {file_path}')
265
+ except Exception as e:
266
+ QMessageBox.critical(gui, '에러', f'DSL 저장 실패: {e}')
267
+
268
+ @staticmethod
269
+ def load_thl_package(gui):
270
+ """.thl 패키지를 로드하여 GUI에 적용"""
271
+ file_path, _ = QFileDialog.getOpenFileName(
272
+ gui, '패키지 로드', '', 'Thumbnail Package (*.thl)'
273
+ )
274
+ if not file_path:
275
+ return
276
+
277
+ try:
278
+ from .dsl_manager import DSLManager
279
+ dsl = DSLManager.load_thl_package(gui, file_path)
280
+ DSLManager.load_dsl_to_gui(gui, dsl)
281
+ QMessageBox.information(gui, '완료', f'패키지 로드 완료: {file_path}')
282
+ except Exception as e:
283
+ QMessageBox.critical(gui, '에러', f'패키지 로드 실패: {e}')
284
+
285
+ @staticmethod
286
+ def save_thl_package(gui):
287
+ """현재 DSL과 사용 폰트를 묶어 .thl 패키지로 저장"""
288
+ file_path, _ = QFileDialog.getSaveFileName(
289
+ gui, '패키지 저장', 'thumbnail.thl', 'Thumbnail Package (*.thl)'
290
+ )
291
+ if not file_path:
292
+ return
293
+ try:
294
+ from .dsl_manager import DSLManager
295
+ DSLManager.save_thl_package(gui, file_path)
296
+ QMessageBox.information(gui, '완료', f'패키지 저장 완료: {file_path}')
297
+ except Exception as e:
298
+ QMessageBox.critical(gui, '에러', f'패키지 저장 실패: {e}')
299
+
@@ -0,0 +1,177 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 메인 GUI 윈도우 클래스
5
+ """
6
+
7
+ import sys
8
+ from PySide6.QtWidgets import QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QTabWidget
9
+
10
+ from .widgets import WidgetFactory
11
+ from .handlers import EventHandlers
12
+ from .dsl_manager import DSLManager
13
+
14
+
15
+ class ThumbnailGUI(QMainWindow):
16
+ """메인 GUI 클래스"""
17
+
18
+ def __init__(self):
19
+ super().__init__()
20
+ self.setWindowTitle('썸네일 생성기')
21
+ self.setGeometry(100, 100, 1200, 800)
22
+
23
+ # 메인 위젯
24
+ main_widget = QWidget()
25
+ self.setCentralWidget(main_widget)
26
+
27
+ # 메인 레이아웃
28
+ main_layout = QHBoxLayout(main_widget)
29
+
30
+ # 왼쪽: 미리보기
31
+ preview_widget = WidgetFactory.create_preview_widget(self)
32
+ main_layout.addWidget(preview_widget, 2)
33
+
34
+ # 오른쪽: 설정 패널
35
+ settings_widget = self.create_settings_widget()
36
+ main_layout.addWidget(settings_widget, 1)
37
+
38
+ # 기본값 초기화
39
+ self.init_default_values()
40
+
41
+ def create_settings_widget(self):
42
+ """설정 위젯 생성"""
43
+ scroll = QWidget()
44
+ layout = QVBoxLayout()
45
+
46
+ # 탭 위젯
47
+ tabs = QTabWidget()
48
+
49
+ # 해상도 탭
50
+ res_tab = WidgetFactory.create_resolution_tab(self)
51
+ tabs.addTab(res_tab, '해상도')
52
+
53
+ # 배경 탭
54
+ bg_tab = WidgetFactory.create_background_tab(self)
55
+ tabs.addTab(bg_tab, '배경')
56
+
57
+ # 제목 탭
58
+ title_tab = WidgetFactory.create_title_tab(self)
59
+ tabs.addTab(title_tab, '제목')
60
+
61
+ # 부제목 탭
62
+ subtitle_tab = WidgetFactory.create_subtitle_tab(self)
63
+ tabs.addTab(subtitle_tab, '부제목')
64
+
65
+ layout.addWidget(tabs)
66
+ scroll.setLayout(layout)
67
+
68
+ return scroll
69
+
70
+ def init_default_values(self):
71
+ """기본값 초기화"""
72
+ self.title_text.setPlainText('10초만에\n썸네일 만드는 법')
73
+ self.subtitle_text.setPlainText('쉽고 빠르게 썸네일을 만드는 법\n= 퀵썸네일 쓰기')
74
+ # 기본 폰트 값 (노느늘 SBAggroB)
75
+ self.title_font_name.setText('SBAggroB')
76
+ self.title_font_url.setText('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/SBAggroB.woff')
77
+ self.title_font_weight.setCurrentText('bold')
78
+ self.title_font_style.setCurrentText('normal')
79
+
80
+ self.subtitle_font_name.setText('SBAggroB')
81
+ self.subtitle_font_url.setText('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/SBAggroB.woff')
82
+ self.subtitle_font_weight.setCurrentText('normal')
83
+ self.subtitle_font_style.setCurrentText('normal')
84
+ self.update_preview()
85
+
86
+ # 이벤트 핸들러 위임
87
+ def on_resolution_mode_changed(self, mode):
88
+ EventHandlers.on_resolution_mode_changed(self, mode)
89
+
90
+ def on_width_changed(self, value):
91
+ EventHandlers.on_width_changed(self, value)
92
+
93
+ def on_height_changed(self, value):
94
+ EventHandlers.on_height_changed(self, value)
95
+
96
+ def on_aspect_ratio_changed(self, ratio):
97
+ EventHandlers.on_aspect_ratio_changed(self, ratio)
98
+
99
+ def select_bg_color(self):
100
+ EventHandlers.select_bg_color(self)
101
+
102
+ def select_title_color(self):
103
+ EventHandlers.select_title_color(self)
104
+
105
+ def select_subtitle_color(self):
106
+ EventHandlers.select_subtitle_color(self)
107
+
108
+ def select_background_image(self):
109
+ EventHandlers.select_background_image(self)
110
+
111
+ def set_title_font_name_from_path(self, path: str):
112
+ EventHandlers.set_title_font_name_from_path(self, path)
113
+
114
+ def set_subtitle_font_name_from_path(self, path: str):
115
+ EventHandlers.set_subtitle_font_name_from_path(self, path)
116
+
117
+ def on_title_font_file_changed(self):
118
+ EventHandlers.on_title_font_file_changed(self)
119
+
120
+ def on_subtitle_font_file_changed(self):
121
+ EventHandlers.on_subtitle_font_file_changed(self)
122
+
123
+ def generate_preview(self):
124
+ EventHandlers.generate_preview(self)
125
+
126
+ def save_thumbnail(self):
127
+ EventHandlers.save_thumbnail(self)
128
+
129
+ def show_dsl_dialog(self):
130
+ EventHandlers.show_dsl_dialog(self)
131
+
132
+ def save_dsl(self):
133
+ EventHandlers.save_dsl(self)
134
+
135
+ def load_thl_package(self):
136
+ EventHandlers.load_thl_package(self)
137
+
138
+ def save_thl_package(self):
139
+ EventHandlers.save_thl_package(self)
140
+
141
+ # DSL 관련 메서드
142
+ def generate_dsl(self):
143
+ return DSLManager.generate_dsl(self)
144
+
145
+ def load_dsl_to_gui(self, dsl: dict):
146
+ DSLManager.load_dsl_to_gui(self, dsl)
147
+
148
+ def update_preview(self):
149
+ """미리보기 업데이트"""
150
+ # URL/로컬 입력 영역 가시성 토글
151
+ is_title_local = self.title_font_source.currentText() == '로컬 폰트 파일'
152
+ self.label_title_font_url.setVisible(not is_title_local)
153
+ self.title_font_url.setVisible(not is_title_local)
154
+ self.label_title_font_file.setVisible(is_title_local)
155
+ self.title_font_file.setVisible(is_title_local)
156
+
157
+ is_sub_local = self.subtitle_font_source.currentText() == '로컬 폰트 파일'
158
+ self.label_subtitle_font_url.setVisible(not is_sub_local)
159
+ self.subtitle_font_url.setVisible(not is_sub_local)
160
+ self.label_subtitle_font_file.setVisible(is_sub_local)
161
+ self.subtitle_font_file.setVisible(is_sub_local)
162
+
163
+ dsl = self.generate_dsl()
164
+ self.current_dsl = dsl
165
+
166
+
167
+ def main():
168
+ """GUI 메인 함수"""
169
+ app = QApplication(sys.argv)
170
+ window = ThumbnailGUI()
171
+ window.show()
172
+ sys.exit(app.exec())
173
+
174
+
175
+ if __name__ == '__main__':
176
+ main()
177
+
@@ -0,0 +1,28 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ 미리보기 생성 스레드
5
+ """
6
+
7
+ from PySide6.QtCore import QThread, Signal
8
+ from ..renderer import ThumbnailRenderer
9
+
10
+
11
+ class PreviewThread(QThread):
12
+ """미리보기 생성 스레드"""
13
+ preview_ready = Signal(str) # preview file path
14
+
15
+ def __init__(self, dsl):
16
+ super().__init__()
17
+ self.dsl = dsl
18
+ self.error_message = None
19
+
20
+ def run(self):
21
+ preview_path = 'preview_temp.png'
22
+ try:
23
+ ThumbnailRenderer.render_thumbnail(self.dsl, preview_path)
24
+ self.preview_ready.emit(preview_path)
25
+ except Exception as e:
26
+ self.error_message = str(e)
27
+ self.preview_ready.emit('')
28
+