thumbnail-maker 0.1.1__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.
Potentially problematic release.
This version of thumbnail-maker might be problematic. Click here for more details.
- thumbnail_maker/__init__.py +9 -0
- thumbnail_maker/__main__.py +71 -0
- thumbnail_maker/cli.py +140 -0
- thumbnail_maker/cli_new.py +38 -0
- thumbnail_maker/gui.py +861 -0
- thumbnail_maker/renderer.py +544 -0
- thumbnail_maker-0.1.1.dist-info/METADATA +158 -0
- thumbnail_maker-0.1.1.dist-info/RECORD +10 -0
- thumbnail_maker-0.1.1.dist-info/WHEEL +4 -0
- thumbnail_maker-0.1.1.dist-info/entry_points.txt +2 -0
thumbnail_maker/gui.py
ADDED
|
@@ -0,0 +1,861 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
PySide6 기반 썸네일 생성 GUI
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import base64
|
|
11
|
+
from PySide6.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout,
|
|
12
|
+
QHBoxLayout, QLabel, QPushButton, QColorDialog,
|
|
13
|
+
QSpinBox, QTextEdit, QCheckBox, QComboBox,
|
|
14
|
+
QGroupBox, QTabWidget, QLineEdit, QSlider,
|
|
15
|
+
QFileDialog, QMessageBox, QGridLayout)
|
|
16
|
+
from PySide6.QtCore import Qt, Signal, QThread
|
|
17
|
+
from PySide6.QtGui import QColor, QFont
|
|
18
|
+
from .renderer import ThumbnailRenderer, sanitize
|
|
19
|
+
try:
|
|
20
|
+
from fontTools.ttLib import TTFont
|
|
21
|
+
except Exception:
|
|
22
|
+
TTFont = None
|
|
23
|
+
import tempfile
|
|
24
|
+
import zipfile
|
|
25
|
+
import shutil
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PreviewThread(QThread):
|
|
29
|
+
"""미리보기 생성 스레드"""
|
|
30
|
+
preview_ready = Signal(str) # preview file path
|
|
31
|
+
|
|
32
|
+
def __init__(self, dsl):
|
|
33
|
+
super().__init__()
|
|
34
|
+
self.dsl = dsl
|
|
35
|
+
self.error_message = None
|
|
36
|
+
|
|
37
|
+
def run(self):
|
|
38
|
+
preview_path = 'preview_temp.png'
|
|
39
|
+
try:
|
|
40
|
+
ThumbnailRenderer.render_thumbnail(self.dsl, preview_path)
|
|
41
|
+
self.preview_ready.emit(preview_path)
|
|
42
|
+
except Exception as e:
|
|
43
|
+
self.error_message = str(e)
|
|
44
|
+
self.preview_ready.emit('')
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class ThumbnailGUI(QMainWindow):
|
|
48
|
+
"""메인 GUI 클래스"""
|
|
49
|
+
|
|
50
|
+
def __init__(self):
|
|
51
|
+
super().__init__()
|
|
52
|
+
self.setWindowTitle('썸네일 생성기')
|
|
53
|
+
self.setGeometry(100, 100, 1200, 800)
|
|
54
|
+
|
|
55
|
+
# 메인 위젯
|
|
56
|
+
main_widget = QWidget()
|
|
57
|
+
self.setCentralWidget(main_widget)
|
|
58
|
+
|
|
59
|
+
# 메인 레이아웃
|
|
60
|
+
main_layout = QHBoxLayout(main_widget)
|
|
61
|
+
|
|
62
|
+
# 왼쪽: 미리보기
|
|
63
|
+
preview_widget = self.create_preview_widget()
|
|
64
|
+
main_layout.addWidget(preview_widget, 2)
|
|
65
|
+
|
|
66
|
+
# 오른쪽: 설정 패널
|
|
67
|
+
settings_widget = self.create_settings_widget()
|
|
68
|
+
main_layout.addWidget(settings_widget, 1)
|
|
69
|
+
|
|
70
|
+
# 기본값 초기화
|
|
71
|
+
self.init_default_values()
|
|
72
|
+
|
|
73
|
+
def create_preview_widget(self):
|
|
74
|
+
"""미리보기 위젯 생성"""
|
|
75
|
+
group = QGroupBox('미리보기')
|
|
76
|
+
layout = QVBoxLayout()
|
|
77
|
+
|
|
78
|
+
self.preview_label = QLabel('미리보기가 여기에 표시됩니다')
|
|
79
|
+
self.preview_label.setMinimumSize(480, 270)
|
|
80
|
+
self.preview_label.setStyleSheet('border: 2px solid gray; background: white;')
|
|
81
|
+
self.preview_label.setAlignment(Qt.AlignCenter)
|
|
82
|
+
|
|
83
|
+
btn_layout = QHBoxLayout()
|
|
84
|
+
|
|
85
|
+
self.preview_btn = QPushButton('미리보기 생성')
|
|
86
|
+
self.preview_btn.clicked.connect(self.generate_preview)
|
|
87
|
+
|
|
88
|
+
self.save_btn = QPushButton('저장')
|
|
89
|
+
self.save_btn.clicked.connect(self.save_thumbnail)
|
|
90
|
+
|
|
91
|
+
self.show_dsl_btn = QPushButton('DSL 보기')
|
|
92
|
+
self.show_dsl_btn.clicked.connect(self.show_dsl_dialog)
|
|
93
|
+
|
|
94
|
+
self.save_dsl_btn = QPushButton('DSL 저장')
|
|
95
|
+
self.save_dsl_btn.clicked.connect(self.save_dsl)
|
|
96
|
+
|
|
97
|
+
self.save_thl_btn = QPushButton('패키지 저장(.thl)')
|
|
98
|
+
self.save_thl_btn.clicked.connect(self.save_thl_package)
|
|
99
|
+
|
|
100
|
+
btn_layout.addWidget(self.preview_btn)
|
|
101
|
+
btn_layout.addWidget(self.save_btn)
|
|
102
|
+
btn_layout.addWidget(self.show_dsl_btn)
|
|
103
|
+
btn_layout.addWidget(self.save_dsl_btn)
|
|
104
|
+
btn_layout.addWidget(self.save_thl_btn)
|
|
105
|
+
|
|
106
|
+
layout.addWidget(self.preview_label)
|
|
107
|
+
layout.addLayout(btn_layout)
|
|
108
|
+
group.setLayout(layout)
|
|
109
|
+
|
|
110
|
+
return group
|
|
111
|
+
|
|
112
|
+
def create_settings_widget(self):
|
|
113
|
+
"""설정 위젯 생성"""
|
|
114
|
+
scroll = QWidget()
|
|
115
|
+
layout = QVBoxLayout()
|
|
116
|
+
|
|
117
|
+
# 탭 위젯
|
|
118
|
+
tabs = QTabWidget()
|
|
119
|
+
|
|
120
|
+
# 해상도 탭
|
|
121
|
+
res_tab = self.create_resolution_tab()
|
|
122
|
+
tabs.addTab(res_tab, '해상도')
|
|
123
|
+
|
|
124
|
+
# 배경 탭
|
|
125
|
+
bg_tab = self.create_background_tab()
|
|
126
|
+
tabs.addTab(bg_tab, '배경')
|
|
127
|
+
|
|
128
|
+
# 제목 탭
|
|
129
|
+
title_tab = self.create_title_tab()
|
|
130
|
+
tabs.addTab(title_tab, '제목')
|
|
131
|
+
|
|
132
|
+
# 부제목 탭
|
|
133
|
+
subtitle_tab = self.create_subtitle_tab()
|
|
134
|
+
tabs.addTab(subtitle_tab, '부제목')
|
|
135
|
+
|
|
136
|
+
layout.addWidget(tabs)
|
|
137
|
+
scroll.setLayout(layout)
|
|
138
|
+
|
|
139
|
+
return scroll
|
|
140
|
+
|
|
141
|
+
def create_resolution_tab(self):
|
|
142
|
+
"""해상도 설정 탭"""
|
|
143
|
+
widget = QWidget()
|
|
144
|
+
layout = QVBoxLayout()
|
|
145
|
+
|
|
146
|
+
# 모드 선택
|
|
147
|
+
self.res_mode = QComboBox()
|
|
148
|
+
self.res_mode.addItems(['preset', 'fixedRatio', 'custom'])
|
|
149
|
+
self.res_mode.currentTextChanged.connect(self.on_resolution_mode_changed)
|
|
150
|
+
|
|
151
|
+
layout.addWidget(QLabel('크기 모드:'))
|
|
152
|
+
layout.addWidget(self.res_mode)
|
|
153
|
+
|
|
154
|
+
# 비율 선택
|
|
155
|
+
self.aspect_ratio = QComboBox()
|
|
156
|
+
self.aspect_ratio.addItems(['16:9', '9:16', '4:3', '1:1'])
|
|
157
|
+
self.aspect_ratio.currentTextChanged.connect(self.update_preview)
|
|
158
|
+
|
|
159
|
+
layout.addWidget(QLabel('비율:'))
|
|
160
|
+
layout.addWidget(self.aspect_ratio)
|
|
161
|
+
|
|
162
|
+
# 너비/높이
|
|
163
|
+
self.width_spin = QSpinBox()
|
|
164
|
+
self.width_spin.setRange(100, 2000)
|
|
165
|
+
self.width_spin.setValue(480)
|
|
166
|
+
self.width_spin.valueChanged.connect(self.update_preview)
|
|
167
|
+
|
|
168
|
+
self.height_spin = QSpinBox()
|
|
169
|
+
self.height_spin.setRange(100, 2000)
|
|
170
|
+
self.height_spin.setValue(270)
|
|
171
|
+
self.height_spin.valueChanged.connect(self.update_preview)
|
|
172
|
+
|
|
173
|
+
layout.addWidget(QLabel('너비:'))
|
|
174
|
+
layout.addWidget(self.width_spin)
|
|
175
|
+
layout.addWidget(QLabel('높이:'))
|
|
176
|
+
layout.addWidget(self.height_spin)
|
|
177
|
+
|
|
178
|
+
layout.addStretch()
|
|
179
|
+
widget.setLayout(layout)
|
|
180
|
+
return widget
|
|
181
|
+
|
|
182
|
+
def create_background_tab(self):
|
|
183
|
+
"""배경 설정 탭"""
|
|
184
|
+
widget = QWidget()
|
|
185
|
+
layout = QVBoxLayout()
|
|
186
|
+
|
|
187
|
+
# 배경 타입
|
|
188
|
+
유형 = QComboBox()
|
|
189
|
+
유형.addItems(['solid', 'gradient', 'image'])
|
|
190
|
+
유형.currentTextChanged.connect(self.update_preview)
|
|
191
|
+
self.bg_type = 유형
|
|
192
|
+
|
|
193
|
+
layout.addWidget(QLabel('배경 타입:'))
|
|
194
|
+
layout.addWidget(유형)
|
|
195
|
+
|
|
196
|
+
# 배경 색상
|
|
197
|
+
self.bg_color_btn = QPushButton('색상 선택')
|
|
198
|
+
self.bg_color_btn.clicked.connect(self.select_bg_color)
|
|
199
|
+
self.bg_color = '#a3e635'
|
|
200
|
+
|
|
201
|
+
layout.addWidget(QLabel('배경 색상:'))
|
|
202
|
+
layout.addWidget(self.bg_color_btn)
|
|
203
|
+
|
|
204
|
+
# 배경 이미지 경로
|
|
205
|
+
self.bg_image_path = QLineEdit()
|
|
206
|
+
self.bg_image_path.setPlaceholderText('이미지 파일 경로')
|
|
207
|
+
|
|
208
|
+
bg_img_btn = QPushButton('이미지 선택')
|
|
209
|
+
bg_img_btn.clicked.connect(self.select_background_image)
|
|
210
|
+
|
|
211
|
+
layout.addWidget(QLabel('배경 이미지:'))
|
|
212
|
+
layout.addWidget(self.bg_image_path)
|
|
213
|
+
layout.addWidget(bg_img_btn)
|
|
214
|
+
|
|
215
|
+
# 이미지 투명도
|
|
216
|
+
self.bg_opacity = QSlider(Qt.Horizontal)
|
|
217
|
+
self.bg_opacity.setRange(0, 100)
|
|
218
|
+
self.bg_opacity.setValue(100)
|
|
219
|
+
self.bg_opacity.valueChanged.connect(self.update_preview)
|
|
220
|
+
|
|
221
|
+
layout.addWidget(QLabel('이미지 투명도:'))
|
|
222
|
+
layout.addWidget(self.bg_opacity)
|
|
223
|
+
|
|
224
|
+
# 이미지 블러
|
|
225
|
+
self.bg_blur = QSlider(Qt.Horizontal)
|
|
226
|
+
self.bg_blur.setRange(0, 20)
|
|
227
|
+
self.bg_blur.setValue(0)
|
|
228
|
+
self.bg_blur.valueChanged.connect(self.update_preview)
|
|
229
|
+
|
|
230
|
+
layout.addWidget(QLabel('이미지 블러:'))
|
|
231
|
+
layout.addWidget(self.bg_blur)
|
|
232
|
+
|
|
233
|
+
layout.addStretch()
|
|
234
|
+
widget.setLayout(layout)
|
|
235
|
+
return widget
|
|
236
|
+
|
|
237
|
+
def create_title_tab(self):
|
|
238
|
+
"""제목 설정 탭"""
|
|
239
|
+
widget = QWidget()
|
|
240
|
+
layout = QVBoxLayout()
|
|
241
|
+
|
|
242
|
+
# 제목 텍스트
|
|
243
|
+
self.title_text = QTextEdit()
|
|
244
|
+
self.title_text.setPlaceholderText('제목 텍스트 입력 (여러 줄 가능)')
|
|
245
|
+
self.title_text.textChanged.connect(self.update_preview)
|
|
246
|
+
|
|
247
|
+
layout.addWidget(QLabel('제목 텍스트:'))
|
|
248
|
+
layout.addWidget(self.title_text)
|
|
249
|
+
|
|
250
|
+
# 폰트 설정 모드 (웹/로컬)
|
|
251
|
+
self.title_font_source = QComboBox()
|
|
252
|
+
self.title_font_source.addItems(['웹 폰트 URL', '로컬 폰트 파일'])
|
|
253
|
+
self.title_font_source.currentTextChanged.connect(self.update_preview)
|
|
254
|
+
|
|
255
|
+
layout.addWidget(QLabel('폰트 소스:'))
|
|
256
|
+
layout.addWidget(self.title_font_source)
|
|
257
|
+
|
|
258
|
+
# 폰트 설정 (이름/URL/굵기/스타일)
|
|
259
|
+
self.title_font_name = QLineEdit()
|
|
260
|
+
self.title_font_name.setPlaceholderText('예: SBAggroB')
|
|
261
|
+
self.title_font_name.textChanged.connect(self.update_preview)
|
|
262
|
+
|
|
263
|
+
self.title_font_url = QLineEdit()
|
|
264
|
+
self.title_font_url.setPlaceholderText('예: https://.../SBAggroB.woff')
|
|
265
|
+
self.title_font_url.textChanged.connect(self.update_preview)
|
|
266
|
+
|
|
267
|
+
# 로컬 파일 경로 + 선택 버튼
|
|
268
|
+
row_title_local = QHBoxLayout()
|
|
269
|
+
self.title_font_file = QLineEdit()
|
|
270
|
+
self.title_font_file.setPlaceholderText('예: C:/Windows/Fonts/malgun.ttf')
|
|
271
|
+
self.title_font_file.textChanged.connect(self.on_title_font_file_changed)
|
|
272
|
+
btn_title_font_browse = QPushButton('찾기')
|
|
273
|
+
def _pick_title_font():
|
|
274
|
+
path, _ = QFileDialog.getOpenFileName(self, '폰트 파일 선택', '', 'Fonts (*.ttf *.otf *.woff *.woff2)')
|
|
275
|
+
if path:
|
|
276
|
+
self.title_font_file.setText(path)
|
|
277
|
+
# 파일 선택 시에는 이름을 강제로 세팅
|
|
278
|
+
self.set_title_font_name_from_path(path)
|
|
279
|
+
btn_title_font_browse.clicked.connect(_pick_title_font)
|
|
280
|
+
row_title_local.addWidget(self.title_font_file)
|
|
281
|
+
row_title_local.addWidget(btn_title_font_browse)
|
|
282
|
+
|
|
283
|
+
self.title_font_weight = QComboBox()
|
|
284
|
+
self.title_font_weight.addItems(['normal', 'bold'])
|
|
285
|
+
self.title_font_weight.currentTextChanged.connect(self.update_preview)
|
|
286
|
+
|
|
287
|
+
self.title_font_style = QComboBox()
|
|
288
|
+
self.title_font_style.addItems(['normal', 'italic'])
|
|
289
|
+
self.title_font_style.currentTextChanged.connect(self.update_preview)
|
|
290
|
+
|
|
291
|
+
layout.addWidget(QLabel('폰트 이름:'))
|
|
292
|
+
layout.addWidget(self.title_font_name)
|
|
293
|
+
# URL/로컬 입력 영역
|
|
294
|
+
self.label_title_font_url = QLabel('폰트 URL (WOFF/WOFF2/TTF):')
|
|
295
|
+
layout.addWidget(self.label_title_font_url)
|
|
296
|
+
layout.addWidget(self.title_font_url)
|
|
297
|
+
self.label_title_font_file = QLabel('로컬 폰트 파일 경로:')
|
|
298
|
+
layout.addWidget(self.label_title_font_file)
|
|
299
|
+
layout.addLayout(row_title_local)
|
|
300
|
+
layout.addWidget(QLabel('폰트 굵기/스타일:'))
|
|
301
|
+
row_font = QHBoxLayout()
|
|
302
|
+
row_font.addWidget(self.title_font_weight)
|
|
303
|
+
row_font.addWidget(self.title_font_style)
|
|
304
|
+
layout.addLayout(row_font)
|
|
305
|
+
|
|
306
|
+
# 제목 색상
|
|
307
|
+
self.title_color_btn = QPushButton('색상 선택')
|
|
308
|
+
self.title_color_btn.clicked.connect(self.select_title_color)
|
|
309
|
+
self.title_color = '#4ade80'
|
|
310
|
+
|
|
311
|
+
layout.addWidget(QLabel('제목 색상:'))
|
|
312
|
+
layout.addWidget(self.title_color_btn)
|
|
313
|
+
|
|
314
|
+
# 폰트 크기
|
|
315
|
+
self.title_font_size = QSpinBox()
|
|
316
|
+
self.title_font_size.setRange(8, 200)
|
|
317
|
+
self.title_font_size.setValue(48)
|
|
318
|
+
self.title_font_size.valueChanged.connect(self.update_preview)
|
|
319
|
+
|
|
320
|
+
layout.addWidget(QLabel('폰트 크기:'))
|
|
321
|
+
layout.addWidget(self.title_font_size)
|
|
322
|
+
|
|
323
|
+
# 외곽선
|
|
324
|
+
self.title_outline_check = QCheckBox('외곽선 사용')
|
|
325
|
+
self.title_outline_check.stateChanged.connect(self.update_preview)
|
|
326
|
+
|
|
327
|
+
self.title_outline_thickness = QSpinBox()
|
|
328
|
+
self.title_outline_thickness.setRange(1, 20)
|
|
329
|
+
self.title_outline_thickness.setValue(7)
|
|
330
|
+
self.title_outline_thickness.valueChanged.connect(self.update_preview)
|
|
331
|
+
|
|
332
|
+
layout.addWidget(self.title_outline_check)
|
|
333
|
+
layout.addWidget(QLabel('외곽선 두께:'))
|
|
334
|
+
layout.addWidget(self.title_outline_thickness)
|
|
335
|
+
|
|
336
|
+
# 위치 (9 그리드)
|
|
337
|
+
self.title_position = QComboBox()
|
|
338
|
+
self.title_position.addItems(['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'])
|
|
339
|
+
self.title_position.currentTextChanged.connect(self.update_preview)
|
|
340
|
+
|
|
341
|
+
layout.addWidget(QLabel('위치:'))
|
|
342
|
+
layout.addWidget(self.title_position)
|
|
343
|
+
|
|
344
|
+
layout.addStretch()
|
|
345
|
+
widget.setLayout(layout)
|
|
346
|
+
return widget
|
|
347
|
+
|
|
348
|
+
def create_subtitle_tab(self):
|
|
349
|
+
"""부제목 설정 탭"""
|
|
350
|
+
widget = QWidget()
|
|
351
|
+
layout = QVBoxLayout()
|
|
352
|
+
|
|
353
|
+
# 부제목 표시 여부
|
|
354
|
+
self.subtitle_visible = QCheckBox('부제목 표시')
|
|
355
|
+
self.subtitle_visible.setChecked(True)
|
|
356
|
+
self.subtitle_visible.stateChanged.connect(self.update_preview)
|
|
357
|
+
|
|
358
|
+
layout.addWidget(self.subtitle_visible)
|
|
359
|
+
|
|
360
|
+
# 부제목 텍스트
|
|
361
|
+
self.subtitle_text = QTextEdit()
|
|
362
|
+
self.subtitle_text.setPlaceholderText('부제목 텍스트 입력 (여러 줄 가능)')
|
|
363
|
+
self.subtitle_text.textChanged.connect(self.update_preview)
|
|
364
|
+
|
|
365
|
+
layout.addWidget(QLabel('부제목 텍스트:'))
|
|
366
|
+
layout.addWidget(self.subtitle_text)
|
|
367
|
+
|
|
368
|
+
# 폰트 설정 모드 (웹/로컬)
|
|
369
|
+
self.subtitle_font_source = QComboBox()
|
|
370
|
+
self.subtitle_font_source.addItems(['웹 폰트 URL', '로컬 폰트 파일'])
|
|
371
|
+
self.subtitle_font_source.currentTextChanged.connect(self.update_preview)
|
|
372
|
+
|
|
373
|
+
layout.addWidget(QLabel('폰트 소스:'))
|
|
374
|
+
layout.addWidget(self.subtitle_font_source)
|
|
375
|
+
|
|
376
|
+
# 폰트 설정 (이름/URL/굵기/스타일)
|
|
377
|
+
self.subtitle_font_name = QLineEdit()
|
|
378
|
+
self.subtitle_font_name.setPlaceholderText('예: SBAggroB')
|
|
379
|
+
self.subtitle_font_name.textChanged.connect(self.update_preview)
|
|
380
|
+
|
|
381
|
+
self.subtitle_font_url = QLineEdit()
|
|
382
|
+
self.subtitle_font_url.setPlaceholderText('예: https://.../SBAggroB.woff')
|
|
383
|
+
self.subtitle_font_url.textChanged.connect(self.update_preview)
|
|
384
|
+
|
|
385
|
+
# 로컬 파일 경로 + 선택 버튼
|
|
386
|
+
row_sub_local = QHBoxLayout()
|
|
387
|
+
self.subtitle_font_file = QLineEdit()
|
|
388
|
+
self.subtitle_font_file.setPlaceholderText('예: C:/Windows/Fonts/malgun.ttf')
|
|
389
|
+
self.subtitle_font_file.textChanged.connect(self.on_subtitle_font_file_changed)
|
|
390
|
+
btn_sub_font_browse = QPushButton('찾기')
|
|
391
|
+
def _pick_sub_font():
|
|
392
|
+
path, _ = QFileDialog.getOpenFileName(self, '폰트 파일 선택', '', 'Fonts (*.ttf *.otf *.woff *.woff2)')
|
|
393
|
+
if path:
|
|
394
|
+
self.subtitle_font_file.setText(path)
|
|
395
|
+
# 파일 선택 시에는 이름을 강제로 세팅
|
|
396
|
+
self.set_subtitle_font_name_from_path(path)
|
|
397
|
+
btn_sub_font_browse.clicked.connect(_pick_sub_font)
|
|
398
|
+
row_sub_local.addWidget(self.subtitle_font_file)
|
|
399
|
+
row_sub_local.addWidget(btn_sub_font_browse)
|
|
400
|
+
|
|
401
|
+
self.subtitle_font_weight = QComboBox()
|
|
402
|
+
self.subtitle_font_weight.addItems(['normal', 'bold'])
|
|
403
|
+
self.subtitle_font_weight.currentTextChanged.connect(self.update_preview)
|
|
404
|
+
|
|
405
|
+
self.subtitle_font_style = QComboBox()
|
|
406
|
+
self.subtitle_font_style.addItems(['normal', 'italic'])
|
|
407
|
+
self.subtitle_font_style.currentTextChanged.connect(self.update_preview)
|
|
408
|
+
|
|
409
|
+
layout.addWidget(QLabel('폰트 이름:'))
|
|
410
|
+
layout.addWidget(self.subtitle_font_name)
|
|
411
|
+
self.label_subtitle_font_url = QLabel('폰트 URL (WOFF/WOFF2/TTF):')
|
|
412
|
+
layout.addWidget(self.label_subtitle_font_url)
|
|
413
|
+
layout.addWidget(self.subtitle_font_url)
|
|
414
|
+
self.label_subtitle_font_file = QLabel('로컬 폰트 파일 경로:')
|
|
415
|
+
layout.addWidget(self.label_subtitle_font_file)
|
|
416
|
+
layout.addLayout(row_sub_local)
|
|
417
|
+
layout.addWidget(QLabel('폰트 굵기/스타일:'))
|
|
418
|
+
row_sub_font = QHBoxLayout()
|
|
419
|
+
row_sub_font.addWidget(self.subtitle_font_weight)
|
|
420
|
+
row_sub_font.addWidget(self.subtitle_font_style)
|
|
421
|
+
layout.addLayout(row_sub_font)
|
|
422
|
+
|
|
423
|
+
# 부제목 색상
|
|
424
|
+
self.subtitle_color_btn = QPushButton('색상 선택')
|
|
425
|
+
self.subtitle_color_btn.clicked.connect(self.select_subtitle_color)
|
|
426
|
+
self.subtitle_color = '#ffffff'
|
|
427
|
+
|
|
428
|
+
layout.addWidget(QLabel('부제목 색상:'))
|
|
429
|
+
layout.addWidget(self.subtitle_color_btn)
|
|
430
|
+
|
|
431
|
+
# 폰트 크기
|
|
432
|
+
self.subtitle_font_size = QSpinBox()
|
|
433
|
+
self.subtitle_font_size.setRange(8, 200)
|
|
434
|
+
self.subtitle_font_size.setValue(24)
|
|
435
|
+
self.subtitle_font_size.valueChanged.connect(self.update_preview)
|
|
436
|
+
|
|
437
|
+
layout.addWidget(QLabel('폰트 크기:'))
|
|
438
|
+
layout.addWidget(self.subtitle_font_size)
|
|
439
|
+
|
|
440
|
+
# 위치 (9 그리드)
|
|
441
|
+
self.subtitle_position = QComboBox()
|
|
442
|
+
self.subtitle_position.addItems(['tl', 'tc', 'tr', 'ml', 'mc', 'mr', 'bl', 'bc', 'br'])
|
|
443
|
+
self.subtitle_position.setCurrentText('bl')
|
|
444
|
+
self.subtitle_position.currentTextChanged.connect(self.update_preview)
|
|
445
|
+
|
|
446
|
+
layout.addWidget(QLabel('위치:'))
|
|
447
|
+
layout.addWidget(self.subtitle_position)
|
|
448
|
+
|
|
449
|
+
layout.addStretch()
|
|
450
|
+
widget.setLayout(layout)
|
|
451
|
+
return widget
|
|
452
|
+
|
|
453
|
+
def init_default_values(self):
|
|
454
|
+
"""기본값 초기화"""
|
|
455
|
+
self.title_text.setPlainText('10초만에\n썸네일 만드는 법')
|
|
456
|
+
self.subtitle_text.setPlainText('쉽고 빠르게 썸네일을 만드는 법\n= 퀵썸네일 쓰기')
|
|
457
|
+
# 기본 폰트 값 (노느늘 SBAggroB)
|
|
458
|
+
self.title_font_name.setText('SBAggroB')
|
|
459
|
+
self.title_font_url.setText('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/SBAggroB.woff')
|
|
460
|
+
self.title_font_weight.setCurrentText('bold')
|
|
461
|
+
self.title_font_style.setCurrentText('normal')
|
|
462
|
+
|
|
463
|
+
self.subtitle_font_name.setText('SBAggroB')
|
|
464
|
+
self.subtitle_font_url.setText('https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/SBAggroB.woff')
|
|
465
|
+
self.subtitle_font_weight.setCurrentText('normal')
|
|
466
|
+
self.subtitle_font_style.setCurrentText('normal')
|
|
467
|
+
self.update_preview()
|
|
468
|
+
|
|
469
|
+
def on_resolution_mode_changed(self, mode):
|
|
470
|
+
"""해상도 모드 변경 시 처리"""
|
|
471
|
+
if mode == 'preset':
|
|
472
|
+
self.aspect_ratio.setEnabled(True)
|
|
473
|
+
elif mode == 'fixedRatio':
|
|
474
|
+
self.aspect_ratio.setEnabled(True)
|
|
475
|
+
else: # custom
|
|
476
|
+
self.aspect_ratio.setEnabled(False)
|
|
477
|
+
self.update_preview()
|
|
478
|
+
|
|
479
|
+
def select_bg_color(self):
|
|
480
|
+
"""배경 색상 선택"""
|
|
481
|
+
color = QColorDialog.getColor(QColor(self.bg_color))
|
|
482
|
+
if color.isValid():
|
|
483
|
+
self.bg_color = color.name()
|
|
484
|
+
self.update_preview()
|
|
485
|
+
|
|
486
|
+
def select_title_color(self):
|
|
487
|
+
"""제목 색상 선택"""
|
|
488
|
+
color = QColorDialog.getColor(QColor(self.title_color))
|
|
489
|
+
if color.isValid():
|
|
490
|
+
self.title_color = color.name()
|
|
491
|
+
self.update_preview()
|
|
492
|
+
|
|
493
|
+
def select_subtitle_color(self):
|
|
494
|
+
"""부제목 색상 선택"""
|
|
495
|
+
color = QColorDialog.getColor(QColor(self.subtitle_color))
|
|
496
|
+
if color.isValid():
|
|
497
|
+
self.subtitle_color = color.name()
|
|
498
|
+
self.update_preview()
|
|
499
|
+
|
|
500
|
+
def select_background_image(self):
|
|
501
|
+
"""배경 이미지 선택"""
|
|
502
|
+
file_path, _ = QFileDialog.getOpenFileName(
|
|
503
|
+
self, '배경 이미지 선택', '', 'Images (*.png *.jpg *.jpeg *.gif *.bmp)'
|
|
504
|
+
)
|
|
505
|
+
if file_path:
|
|
506
|
+
self.bg_image_path.setText(file_path)
|
|
507
|
+
self.update_preview()
|
|
508
|
+
|
|
509
|
+
def generate_dsl(self):
|
|
510
|
+
"""DSL 생성"""
|
|
511
|
+
# 해상도 결정
|
|
512
|
+
res_mode = self.res_mode.currentText()
|
|
513
|
+
if res_mode == 'preset':
|
|
514
|
+
resolution = {
|
|
515
|
+
'type': 'preset',
|
|
516
|
+
'value': self.aspect_ratio.currentText()
|
|
517
|
+
}
|
|
518
|
+
elif res_mode == 'fixedRatio':
|
|
519
|
+
resolution = {
|
|
520
|
+
'type': 'fixedRatio',
|
|
521
|
+
'ratioValue': self.aspect_ratio.currentText(),
|
|
522
|
+
'width': self.width_spin.value()
|
|
523
|
+
}
|
|
524
|
+
else: # custom
|
|
525
|
+
resolution = {
|
|
526
|
+
'type': 'custom',
|
|
527
|
+
'width': self.width_spin.value(),
|
|
528
|
+
'height': self.height_spin.value()
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
# 배경 결정
|
|
532
|
+
bg_type = self.bg_type.currentText()
|
|
533
|
+
if bg_type == 'image' and self.bg_image_path.text():
|
|
534
|
+
# 이미지를 base64로 변환
|
|
535
|
+
with open(self.bg_image_path.text(), 'rb') as f:
|
|
536
|
+
image_data = f.read()
|
|
537
|
+
base64_str = base64.b64encode(image_data).decode('utf-8')
|
|
538
|
+
data_url = f"data:image/png;base64,{base64_str}"
|
|
539
|
+
|
|
540
|
+
background = {
|
|
541
|
+
'type': 'image',
|
|
542
|
+
'imagePath': data_url,
|
|
543
|
+
'imageOpacity': self.bg_opacity.value() / 100.0,
|
|
544
|
+
'imageBlur': self.bg_blur.value()
|
|
545
|
+
}
|
|
546
|
+
elif bg_type == 'gradient':
|
|
547
|
+
background = {
|
|
548
|
+
'type': 'gradient',
|
|
549
|
+
'colors': [self.bg_color, '#000000']
|
|
550
|
+
}
|
|
551
|
+
else: # solid
|
|
552
|
+
background = {
|
|
553
|
+
'type': 'solid',
|
|
554
|
+
'color': self.bg_color
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
# 텍스트 설정
|
|
558
|
+
# 제목 폰트 소스 분기
|
|
559
|
+
title_use_local = self.title_font_source.currentText() == '로컬 폰트 파일'
|
|
560
|
+
title_face_url = self.title_font_file.text() if title_use_local and self.title_font_file.text() else (self.title_font_url.text() or 'https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/SBAggroB.woff')
|
|
561
|
+
# 부제목 폰트 소스 분기
|
|
562
|
+
subtitle_use_local = self.subtitle_font_source.currentText() == '로컬 폰트 파일'
|
|
563
|
+
subtitle_face_url = self.subtitle_font_file.text() if subtitle_use_local and self.subtitle_font_file.text() else (self.subtitle_font_url.text() or 'https://fastly.jsdelivr.net/gh/projectnoonnu/noonfonts_2108@1.1/SBAggroB.woff')
|
|
564
|
+
|
|
565
|
+
texts = [
|
|
566
|
+
{
|
|
567
|
+
'type': 'title',
|
|
568
|
+
'content': self.title_text.toPlainText(),
|
|
569
|
+
'gridPosition': self.title_position.currentText(),
|
|
570
|
+
'font': {
|
|
571
|
+
'name': self.title_font_name.text() or 'SBAggroB',
|
|
572
|
+
'faces': [{
|
|
573
|
+
'name': self.title_font_name.text() or 'SBAggroB',
|
|
574
|
+
'url': title_face_url,
|
|
575
|
+
'weight': self.title_font_weight.currentText() or 'bold',
|
|
576
|
+
'style': self.title_font_style.currentText() or 'normal'
|
|
577
|
+
}]
|
|
578
|
+
},
|
|
579
|
+
'fontSize': self.title_font_size.value(),
|
|
580
|
+
'color': self.title_color,
|
|
581
|
+
'fontWeight': self.title_font_weight.currentText() or 'bold',
|
|
582
|
+
'fontStyle': self.title_font_style.currentText() or 'normal',
|
|
583
|
+
'lineHeight': 1.1,
|
|
584
|
+
'wordWrap': False,
|
|
585
|
+
'outline': {
|
|
586
|
+
'thickness': self.title_outline_thickness.value(),
|
|
587
|
+
'color': '#000000'
|
|
588
|
+
} if self.title_outline_check.isChecked() else None,
|
|
589
|
+
'enabled': True
|
|
590
|
+
}
|
|
591
|
+
]
|
|
592
|
+
|
|
593
|
+
# 부제목 추가
|
|
594
|
+
if self.subtitle_visible.isChecked():
|
|
595
|
+
texts.append({
|
|
596
|
+
'type': 'subtitle',
|
|
597
|
+
'content': self.subtitle_text.toPlainText(),
|
|
598
|
+
'gridPosition': self.subtitle_position.currentText(),
|
|
599
|
+
'font': {
|
|
600
|
+
'name': self.subtitle_font_name.text() or 'SBAggroB',
|
|
601
|
+
'faces': [{
|
|
602
|
+
'name': self.subtitle_font_name.text() or 'SBAggroB',
|
|
603
|
+
'url': subtitle_face_url,
|
|
604
|
+
'weight': self.subtitle_font_weight.currentText() or 'normal',
|
|
605
|
+
'style': self.subtitle_font_style.currentText() or 'normal'
|
|
606
|
+
}]
|
|
607
|
+
},
|
|
608
|
+
'fontSize': self.subtitle_font_size.value(),
|
|
609
|
+
'color': self.subtitle_color,
|
|
610
|
+
'fontWeight': self.subtitle_font_weight.currentText() or 'normal',
|
|
611
|
+
'fontStyle': self.subtitle_font_style.currentText() or 'normal',
|
|
612
|
+
'lineHeight': 1.1,
|
|
613
|
+
'wordWrap': False,
|
|
614
|
+
'outline': None,
|
|
615
|
+
'enabled': True
|
|
616
|
+
})
|
|
617
|
+
|
|
618
|
+
dsl = {
|
|
619
|
+
'Thumbnail': {
|
|
620
|
+
'Resolution': resolution,
|
|
621
|
+
'Background': background,
|
|
622
|
+
'Texts': texts
|
|
623
|
+
},
|
|
624
|
+
'TemplateMeta': {
|
|
625
|
+
'name': '',
|
|
626
|
+
'shareable': False
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
return dsl
|
|
631
|
+
|
|
632
|
+
def update_preview(self):
|
|
633
|
+
"""미리보기 업데이트"""
|
|
634
|
+
# URL/로컬 입력 영역 가시성 토글
|
|
635
|
+
is_title_local = self.title_font_source.currentText() == '로컬 폰트 파일'
|
|
636
|
+
self.label_title_font_url.setVisible(not is_title_local)
|
|
637
|
+
self.title_font_url.setVisible(not is_title_local)
|
|
638
|
+
self.label_title_font_file.setVisible(is_title_local)
|
|
639
|
+
self.title_font_file.setVisible(is_title_local)
|
|
640
|
+
# 파일 찾기 버튼은 레이아웃에 포함되어 있어 개별 위젯 접근 불가하므로 입력창 표시로 충분
|
|
641
|
+
|
|
642
|
+
is_sub_local = self.subtitle_font_source.currentText() == '로컬 폰트 파일'
|
|
643
|
+
self.label_subtitle_font_url.setVisible(not is_sub_local)
|
|
644
|
+
self.subtitle_font_url.setVisible(not is_sub_local)
|
|
645
|
+
self.label_subtitle_font_file.setVisible(is_sub_local)
|
|
646
|
+
self.subtitle_font_file.setVisible(is_sub_local)
|
|
647
|
+
|
|
648
|
+
dsl = self.generate_dsl()
|
|
649
|
+
self.current_dsl = dsl
|
|
650
|
+
|
|
651
|
+
# ---------- 폰트 이름 자동 추출 ----------
|
|
652
|
+
@staticmethod
|
|
653
|
+
def _infer_font_name_from_file(file_path: str) -> str:
|
|
654
|
+
try:
|
|
655
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
656
|
+
if TTFont and ext in ('.ttf', '.otf') and os.path.exists(file_path):
|
|
657
|
+
tt = TTFont(file_path)
|
|
658
|
+
# Prefer full font name (nameID=4), fallback to font family (nameID=1)
|
|
659
|
+
name = None
|
|
660
|
+
for rec in tt['name'].names:
|
|
661
|
+
if rec.nameID in (4, 1):
|
|
662
|
+
try:
|
|
663
|
+
val = rec.toUnicode()
|
|
664
|
+
except Exception:
|
|
665
|
+
val = rec.string.decode(rec.getEncoding(), errors='ignore')
|
|
666
|
+
if val:
|
|
667
|
+
name = val
|
|
668
|
+
if rec.nameID == 4:
|
|
669
|
+
break
|
|
670
|
+
if name:
|
|
671
|
+
return name
|
|
672
|
+
except Exception:
|
|
673
|
+
pass
|
|
674
|
+
# Fallback: 파일명(확장자 제외)
|
|
675
|
+
return os.path.splitext(os.path.basename(file_path))[0]
|
|
676
|
+
|
|
677
|
+
def set_title_font_name_from_path(self, path: str) -> None:
|
|
678
|
+
inferred = self._infer_font_name_from_file(path)
|
|
679
|
+
if inferred:
|
|
680
|
+
self.title_font_name.setText(inferred)
|
|
681
|
+
self.update_preview()
|
|
682
|
+
|
|
683
|
+
def set_subtitle_font_name_from_path(self, path: str) -> None:
|
|
684
|
+
inferred = self._infer_font_name_from_file(path)
|
|
685
|
+
if inferred:
|
|
686
|
+
self.subtitle_font_name.setText(inferred)
|
|
687
|
+
self.update_preview()
|
|
688
|
+
|
|
689
|
+
def on_title_font_file_changed(self):
|
|
690
|
+
# 경로를 직접 입력한 경우: 이름 칸이 비어 있을 때만 채움
|
|
691
|
+
path = self.title_font_file.text().strip()
|
|
692
|
+
if path and not self.title_font_name.text().strip():
|
|
693
|
+
self.set_title_font_name_from_path(path)
|
|
694
|
+
else:
|
|
695
|
+
self.update_preview()
|
|
696
|
+
|
|
697
|
+
def on_subtitle_font_file_changed(self):
|
|
698
|
+
# 경로를 직접 입력한 경우: 이름 칸이 비어 있을 때만 채움
|
|
699
|
+
path = self.subtitle_font_file.text().strip()
|
|
700
|
+
if path and not self.subtitle_font_name.text().strip():
|
|
701
|
+
self.set_subtitle_font_name_from_path(path)
|
|
702
|
+
else:
|
|
703
|
+
self.update_preview()
|
|
704
|
+
|
|
705
|
+
def generate_preview(self):
|
|
706
|
+
"""미리보기 생성"""
|
|
707
|
+
if not hasattr(self, 'current_dsl'):
|
|
708
|
+
self.update_preview()
|
|
709
|
+
|
|
710
|
+
self.preview_btn.setEnabled(False)
|
|
711
|
+
self.preview_btn.setText('생성 중...')
|
|
712
|
+
|
|
713
|
+
# 스레드에서 생성
|
|
714
|
+
self.preview_thread = PreviewThread(self.current_dsl)
|
|
715
|
+
self.preview_thread.preview_ready.connect(self.on_preview_ready)
|
|
716
|
+
self.preview_thread.start()
|
|
717
|
+
|
|
718
|
+
def on_preview_ready(self, file_path):
|
|
719
|
+
"""미리보기 준비됨"""
|
|
720
|
+
from PySide6.QtGui import QPixmap
|
|
721
|
+
if file_path and os.path.exists(file_path):
|
|
722
|
+
pixmap = QPixmap(file_path)
|
|
723
|
+
self.preview_label.setPixmap(pixmap.scaled(
|
|
724
|
+
480, 270, Qt.KeepAspectRatio, Qt.SmoothTransformation
|
|
725
|
+
))
|
|
726
|
+
else:
|
|
727
|
+
msg = self.preview_thread.error_message or '미리보기 생성 중 오류가 발생했습니다.'
|
|
728
|
+
QMessageBox.critical(self, '에러', msg)
|
|
729
|
+
|
|
730
|
+
self.preview_btn.setEnabled(True)
|
|
731
|
+
self.preview_btn.setText('미리보기 생성')
|
|
732
|
+
|
|
733
|
+
def save_thumbnail(self):
|
|
734
|
+
"""썸네일 저장"""
|
|
735
|
+
if not hasattr(self, 'current_dsl'):
|
|
736
|
+
QMessageBox.warning(self, '경고', '먼저 미리보기를 생성해주세요.')
|
|
737
|
+
return
|
|
738
|
+
|
|
739
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
740
|
+
self, '썸네일 저장', 'thumbnail.png', 'Images (*.png)'
|
|
741
|
+
)
|
|
742
|
+
|
|
743
|
+
if file_path:
|
|
744
|
+
try:
|
|
745
|
+
ThumbnailRenderer.render_thumbnail(self.current_dsl, file_path)
|
|
746
|
+
QMessageBox.information(self, '완료', f'저장 완료: {file_path}')
|
|
747
|
+
except Exception as e:
|
|
748
|
+
QMessageBox.critical(self, '에러', f'저장 실패: {e}')
|
|
749
|
+
|
|
750
|
+
def show_dsl_dialog(self):
|
|
751
|
+
"""현재 DSL을 JSON으로 출력하는 다이얼로그"""
|
|
752
|
+
if not hasattr(self, 'current_dsl'):
|
|
753
|
+
self.update_preview()
|
|
754
|
+
dsl = getattr(self, 'current_dsl', self.generate_dsl())
|
|
755
|
+
try:
|
|
756
|
+
text = json.dumps(dsl, ensure_ascii=False, indent=2)
|
|
757
|
+
except Exception:
|
|
758
|
+
text = str(dsl)
|
|
759
|
+
|
|
760
|
+
from PySide6.QtWidgets import QDialog, QVBoxLayout, QPlainTextEdit, QDialogButtonBox
|
|
761
|
+
dlg = QDialog(self)
|
|
762
|
+
dlg.setWindowTitle('현재 DSL 보기')
|
|
763
|
+
v = QVBoxLayout(dlg)
|
|
764
|
+
editor = QPlainTextEdit()
|
|
765
|
+
editor.setPlainText(text)
|
|
766
|
+
editor.setReadOnly(True)
|
|
767
|
+
v.addWidget(editor)
|
|
768
|
+
btns = QDialogButtonBox(QDialogButtonBox.Close)
|
|
769
|
+
btns.rejected.connect(dlg.reject)
|
|
770
|
+
btns.accepted.connect(dlg.accept)
|
|
771
|
+
v.addWidget(btns)
|
|
772
|
+
dlg.resize(700, 500)
|
|
773
|
+
dlg.exec()
|
|
774
|
+
|
|
775
|
+
def save_dsl(self):
|
|
776
|
+
"""현재 DSL을 JSON 파일로 저장"""
|
|
777
|
+
if not hasattr(self, 'current_dsl'):
|
|
778
|
+
self.update_preview()
|
|
779
|
+
dsl = getattr(self, 'current_dsl', self.generate_dsl())
|
|
780
|
+
|
|
781
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
782
|
+
self, 'DSL 저장', 'thumbnail.json', 'JSON (*.json)'
|
|
783
|
+
)
|
|
784
|
+
if not file_path:
|
|
785
|
+
return
|
|
786
|
+
try:
|
|
787
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
788
|
+
json.dump(dsl, f, ensure_ascii=False, indent=2)
|
|
789
|
+
QMessageBox.information(self, '완료', f'DSL 저장 완료: {file_path}')
|
|
790
|
+
except Exception as e:
|
|
791
|
+
QMessageBox.critical(self, '에러', f'DSL 저장 실패: {e}')
|
|
792
|
+
|
|
793
|
+
def save_thl_package(self):
|
|
794
|
+
"""현재 DSL과 사용 폰트를 묶어 .thl 패키지로 저장"""
|
|
795
|
+
if not hasattr(self, 'current_dsl'):
|
|
796
|
+
self.update_preview()
|
|
797
|
+
dsl = getattr(self, 'current_dsl', self.generate_dsl())
|
|
798
|
+
|
|
799
|
+
# 저장 경로 선택
|
|
800
|
+
file_path, _ = QFileDialog.getSaveFileName(
|
|
801
|
+
self, '패키지 저장', 'thumbnail.thl', 'Thumbnail Package (*.thl)'
|
|
802
|
+
)
|
|
803
|
+
if not file_path:
|
|
804
|
+
return
|
|
805
|
+
try:
|
|
806
|
+
# 스테이징 디렉토리 구성
|
|
807
|
+
staging = tempfile.mkdtemp(prefix='thl_pkg_')
|
|
808
|
+
fonts_dir = os.path.join(staging, 'fonts')
|
|
809
|
+
os.makedirs(fonts_dir, exist_ok=True)
|
|
810
|
+
|
|
811
|
+
# 폰트 확보 및 복사
|
|
812
|
+
texts = dsl.get('Thumbnail', {}).get('Texts', [])
|
|
813
|
+
try:
|
|
814
|
+
# 프로젝트/fonts에 TTF 생성/보장
|
|
815
|
+
ThumbnailRenderer.ensure_fonts(texts)
|
|
816
|
+
except Exception as e:
|
|
817
|
+
print(f"폰트 확보 경고: {e}")
|
|
818
|
+
|
|
819
|
+
# faces를 순회하여 예상 파일명으로 복사
|
|
820
|
+
faces = ThumbnailRenderer.parse_font_faces(texts)
|
|
821
|
+
copied = 0
|
|
822
|
+
for face in faces:
|
|
823
|
+
ttf_name = f"{sanitize(face.get('name','Font'))}-{sanitize(str(face.get('weight','normal')))}-{sanitize(str(face.get('style','normal')))}.ttf"
|
|
824
|
+
src_path = os.path.join(ThumbnailRenderer._fonts_dir(), ttf_name)
|
|
825
|
+
if os.path.exists(src_path):
|
|
826
|
+
shutil.copy2(src_path, os.path.join(fonts_dir, ttf_name))
|
|
827
|
+
copied += 1
|
|
828
|
+
|
|
829
|
+
# thumbnail.json 저장 (원본 DSL 그대로)
|
|
830
|
+
with open(os.path.join(staging, 'thumbnail.json'), 'w', encoding='utf-8') as f:
|
|
831
|
+
json.dump(dsl, f, ensure_ascii=False, indent=2)
|
|
832
|
+
|
|
833
|
+
# zip -> .thl
|
|
834
|
+
with zipfile.ZipFile(file_path, 'w', compression=zipfile.ZIP_DEFLATED) as zf:
|
|
835
|
+
# 루트에 thumbnail.json
|
|
836
|
+
zf.write(os.path.join(staging, 'thumbnail.json'), arcname='thumbnail.json')
|
|
837
|
+
# fonts 폴더
|
|
838
|
+
if os.path.isdir(fonts_dir):
|
|
839
|
+
for name in os.listdir(fonts_dir):
|
|
840
|
+
zf.write(os.path.join(fonts_dir, name), arcname=os.path.join('fonts', name))
|
|
841
|
+
|
|
842
|
+
QMessageBox.information(self, '완료', f'패키지 저장 완료: {file_path}')
|
|
843
|
+
except Exception as e:
|
|
844
|
+
QMessageBox.critical(self, '에러', f'패키지 저장 실패: {e}')
|
|
845
|
+
finally:
|
|
846
|
+
try:
|
|
847
|
+
shutil.rmtree(staging, ignore_errors=True)
|
|
848
|
+
except Exception:
|
|
849
|
+
pass
|
|
850
|
+
|
|
851
|
+
|
|
852
|
+
def main():
|
|
853
|
+
app = QApplication(sys.argv)
|
|
854
|
+
window = ThumbnailGUI()
|
|
855
|
+
window.show()
|
|
856
|
+
sys.exit(app.exec())
|
|
857
|
+
|
|
858
|
+
|
|
859
|
+
if __name__ == '__main__':
|
|
860
|
+
main()
|
|
861
|
+
|