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,279 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ 이미지 업로드 모듈
4
+ """
5
+
6
+ import asyncio
7
+ import os
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Awaitable, Callable, Dict, Optional
11
+
12
+ import httpx
13
+ from loguru import logger
14
+
15
+
16
+ def get_img_ext(img: bytes) -> str:
17
+ """이미지 바이너리에서 확장자 추출"""
18
+ if not img:
19
+ return "bin"
20
+ if img[:2] == b"\xff\xd8":
21
+ return "jpg"
22
+ if img[:8] == b"\x89PNG\r\n\x1a\n":
23
+ return "png"
24
+ if img[:6] in (b"GIF87a", b"GIF89a"):
25
+ return "gif"
26
+ if len(img) > 12 and img[:4] == b"RIFF" and img[8:12] == b"WEBP":
27
+ return "webp"
28
+ if img[:2] == b"BM":
29
+ return "bmp"
30
+ if img[:4] in (b"II*\x00", b"MM\x00*"):
31
+ return "tiff"
32
+ return "bin"
33
+
34
+
35
+ def log_on_error(response: httpx.Response):
36
+ """에러 로깅"""
37
+ logger.error(f"Request failed: [{response.status_code}] {response.request.method} {response.url}")
38
+ try:
39
+ logger.debug(f"Response Body: {response.text[:500]}...")
40
+ except Exception as e:
41
+ logger.warning(f"Could not log response body: {e}")
42
+
43
+
44
+ # --- 개별 업로드 함수들 --- #
45
+
46
+ @logger.catch(message="Error in anhmoe_upload", default=None)
47
+ async def anhmoe_upload(client: httpx.AsyncClient, img: bytes) -> Optional[str]:
48
+ """anhmoe 업로드"""
49
+ response = await client.post(
50
+ "https://anh.moe/api/1/upload",
51
+ data={"key": "anh.moe_public_api"},
52
+ files={"source": img},
53
+ timeout=60,
54
+ )
55
+ if response.is_error:
56
+ log_on_error(response)
57
+ return None
58
+ try:
59
+ return response.json()["image"]["url"]
60
+ except Exception as e:
61
+ logger.error(f"anhmoe parse error: {e} - Resp: {response.text}")
62
+ return None
63
+
64
+
65
+ @logger.catch(message="Error in beeimg_upload", default=None)
66
+ async def beeimg_upload(client: httpx.AsyncClient, img: bytes) -> Optional[str]:
67
+ """beeimg 업로드"""
68
+ ext = get_img_ext(img)
69
+ if ext == "bin":
70
+ logger.warning("Beeimg: Skip unknown ext")
71
+ return None
72
+ name = f"image.{ext}"
73
+ content_type = f"image/{ext}"
74
+ logger.debug(f"Beeimg: Uploading {name} type: {content_type}")
75
+ response = await client.post(
76
+ "https://beeimg.com/api/upload/file/json/",
77
+ files={"file": (name, img, content_type)},
78
+ timeout=60,
79
+ )
80
+ if response.is_error:
81
+ log_on_error(response)
82
+ try:
83
+ logger.error(f"Beeimg API Error: {response.json()}")
84
+ except Exception:
85
+ pass
86
+ return None
87
+ try:
88
+ relative_url = response.json().get("files", {}).get("url")
89
+ if relative_url:
90
+ return f"https:{relative_url}" if relative_url.startswith("//") else relative_url
91
+ else:
92
+ logger.error(f"beeimg missing URL: {response.text}")
93
+ return None
94
+ except Exception as e:
95
+ logger.error(f"beeimg parse error: {e} - Resp: {response.text}")
96
+ return None
97
+
98
+
99
+ @logger.catch(message="Error in fastpic_upload", default=None)
100
+ async def fastpic_upload(client: httpx.AsyncClient, img: bytes) -> Optional[str]:
101
+ """fastpic 업로드"""
102
+ response = await client.post(
103
+ "https://fastpic.org/upload?api=1",
104
+ data={"method": "file", "check_thumb": "no", "uploading": "1"},
105
+ files={"file1": img},
106
+ timeout=60,
107
+ )
108
+ if response.is_error:
109
+ log_on_error(response)
110
+ return None
111
+ match = re.search(r"<imagepath>(.+?)</imagepath>", response.text)
112
+ if match:
113
+ return match[1].strip()
114
+ else:
115
+ logger.error(f"fastpic missing imagepath: {response.text}")
116
+ return None
117
+
118
+
119
+ @logger.catch(message="Error in imagebin_upload", default=None)
120
+ async def imagebin_upload(client: httpx.AsyncClient, img: bytes) -> Optional[str]:
121
+ """imagebin 업로드"""
122
+ response = await client.post(url="https://imagebin.ca/upload.php", files={"file": img}, timeout=60)
123
+ if response.is_error:
124
+ log_on_error(response)
125
+ return None
126
+ match = re.search(r"url:\s*(.+?)$", response.text, flags=re.MULTILINE)
127
+ if match:
128
+ return match[1].strip()
129
+ else:
130
+ logger.error(f"imagebin missing URL pattern: {response.text}")
131
+ return None
132
+
133
+
134
+ @logger.catch(message="Error in pixhost_upload", default=None)
135
+ async def pixhost_upload(client: httpx.AsyncClient, img: bytes) -> Optional[str]:
136
+ """pixhost 업로드"""
137
+ try:
138
+ response = await client.post(
139
+ "https://api.pixhost.to/images",
140
+ data={"content_type": 0},
141
+ files={"img": img},
142
+ timeout=60,
143
+ )
144
+ response.raise_for_status()
145
+ json_response = response.json()
146
+ show_url = json_response.get("show_url")
147
+ direct_image_url = json_response.get("url")
148
+ result = direct_image_url if direct_image_url else show_url
149
+ if result:
150
+ return result
151
+ else:
152
+ logger.error(f"pixhost missing URL/show_url: {json_response}")
153
+ return None
154
+ except httpx.HTTPStatusError as e:
155
+ if e.response.status_code == 414:
156
+ logger.error(f"Pixhost 414 type: {get_img_ext(img)}")
157
+ log_on_error(e.response)
158
+ return None
159
+ except httpx.RequestError as e:
160
+ logger.error(f"Pixhost request failed: {e}")
161
+ return None
162
+ except Exception as e:
163
+ logger.error(f"Pixhost general error: {e} - Resp: {getattr(response, 'text', 'N/A')}")
164
+ return None
165
+
166
+
167
+ @logger.catch(message="Error in sxcu_upload", default=None)
168
+ async def sxcu_upload(client: httpx.AsyncClient, img: bytes, retry_delay=5) -> Optional[str]:
169
+ """sxcu 업로드"""
170
+ headers = {"User-Agent": "Mozilla/5.0"}
171
+ try:
172
+ response = await client.post(
173
+ "https://sxcu.net/api/files/create",
174
+ headers=headers,
175
+ files={"file": img},
176
+ timeout=60,
177
+ )
178
+ if response.status_code == 429:
179
+ logger.warning(f"Sxcu rate limit (429). Wait {retry_delay}s...")
180
+ await asyncio.sleep(retry_delay)
181
+ logger.info("Retrying sxcu upload...")
182
+ response = await client.post(
183
+ "https://sxcu.net/api/files/create",
184
+ headers=headers,
185
+ files={"file": img},
186
+ timeout=60,
187
+ )
188
+ if response.is_error:
189
+ log_on_error(response)
190
+ return None
191
+ json_data = response.json()
192
+ base_url = json_data.get("url")
193
+ if base_url:
194
+ return base_url
195
+ else:
196
+ logger.error(f"sxcu missing URL/error: {json_data.get('error', 'Unknown')} - Resp: {response.text}")
197
+ return None
198
+ except httpx.RequestError as e:
199
+ logger.error(f"sxcu request failed: {e}")
200
+ return None
201
+ except Exception as e:
202
+ logger.error(f"sxcu general error: {e} - Resp: {getattr(response, 'text', 'N/A')}")
203
+ return None
204
+
205
+
206
+ # --- 업로드 대상 서비스 모음 --- #
207
+
208
+ UPLOAD_TARGETS: Dict[str, Callable[[httpx.AsyncClient, bytes], Awaitable[Optional[str]]]] = {
209
+ "anhmoe": anhmoe_upload,
210
+ "beeimg": beeimg_upload,
211
+ "fastpic": fastpic_upload,
212
+ "imagebin": imagebin_upload,
213
+ "pixhost": pixhost_upload,
214
+ "sxcu": sxcu_upload,
215
+ }
216
+
217
+
218
+ async def upload_file_async(file_path: str) -> Optional[str]:
219
+ """
220
+ 파일을 업로드하고 URL을 반환합니다.
221
+
222
+ Args:
223
+ file_path: 업로드할 파일 경로
224
+
225
+ Returns:
226
+ 업로드된 URL 또는 None (실패 시)
227
+ """
228
+ if not os.path.exists(file_path):
229
+ logger.error(f"파일을 찾을 수 없습니다: {file_path}")
230
+ return None
231
+
232
+ # 파일 읽기
233
+ try:
234
+ with open(file_path, "rb") as f:
235
+ img_data = f.read()
236
+ except Exception as e:
237
+ logger.error(f"파일 읽기 실패: {file_path}, {e}")
238
+ return None
239
+
240
+ if not img_data:
241
+ logger.error(f"파일이 비어있습니다: {file_path}")
242
+ return None
243
+
244
+ headers = {
245
+ "User-Agent": "Mozilla/5.0",
246
+ "Accept": "image/*,*/*;q=0.8",
247
+ }
248
+
249
+ # 각 서비스를 순차적으로 시도
250
+ async with httpx.AsyncClient(timeout=60, follow_redirects=True, headers=headers) as client:
251
+ for service_name, upload_func in UPLOAD_TARGETS.items():
252
+ try:
253
+ logger.info(f"[{service_name}] 업로드 시도 중...")
254
+ result_url = await upload_func(client, img_data)
255
+ if result_url:
256
+ logger.success(f"[{service_name}] 업로드 성공: {result_url}")
257
+ return result_url
258
+ else:
259
+ logger.warning(f"[{service_name}] 업로드 실패, 다음 서비스 시도...")
260
+ except Exception as e:
261
+ logger.error(f"[{service_name}] 업로드 중 오류: {e}")
262
+ continue
263
+
264
+ logger.error("모든 업로드 서비스 실패")
265
+ return None
266
+
267
+
268
+ def upload_file(file_path: str) -> Optional[str]:
269
+ """
270
+ 파일을 업로드하고 URL을 반환합니다 (동기 함수).
271
+
272
+ Args:
273
+ file_path: 업로드할 파일 경로
274
+
275
+ Returns:
276
+ 업로드된 URL 또는 None (실패 시)
277
+ """
278
+ return asyncio.run(upload_file_async(file_path))
279
+
@@ -0,0 +1,159 @@
1
+ Metadata-Version: 2.4
2
+ Name: thumbnail-maker
3
+ Version: 0.1.6
4
+ Summary: 썸네일 생성 도구 - Pillow 기반
5
+ Requires-Python: >=3.10
6
+ Requires-Dist: fonttools>=4.47.0
7
+ Requires-Dist: pillow>=10.0.0
8
+ Requires-Dist: requests>=2.31.0
9
+ Requires-Dist: uv-easy>=0.2.5
10
+ Provides-Extra: gui
11
+ Requires-Dist: pyside6>=6.10.0; extra == 'gui'
12
+ Description-Content-Type: text/markdown
13
+
14
+ # 썸네일 생성기 (Python)
15
+
16
+ JavaScript 기반 썸네일 생성기를 Python으로 변환한 프로젝트입니다.
17
+
18
+ ## 주요 변경사항
19
+
20
+ - **Pillow**: 이미지 생성 라이브러리로 사용
21
+ - **PySide6**: GUI 프레임워크로 사용
22
+ - **Python**: 모든 코드를 Python으로 변환
23
+
24
+ ## 설치 방법
25
+
26
+ ```bash
27
+ pip install -r requirements.txt
28
+ ```
29
+
30
+ ## 사용 방법
31
+
32
+ uv를 사용하여 설치:
33
+
34
+ ```bash
35
+ uv sync
36
+ ```
37
+
38
+ 또는 직접 실행:
39
+
40
+ ```bash
41
+ uv run python -m thumbnail_maker
42
+ ```
43
+
44
+ ### 1. GUI 사용 (추천)
45
+
46
+ ```bash
47
+ uv run thumbnail-gui
48
+ ```
49
+
50
+ PySide6 기반 GUI에서 썸네일을 생성할 수 있습니다.
51
+
52
+ ### 2. CLI 사용
53
+
54
+ #### 기본 사용
55
+ ```bash
56
+ uv run generate-thumbnail
57
+ ```
58
+
59
+ #### DSL 파일 지정
60
+ ```bash
61
+ uv run generate-thumbnail mydsl.json -o output.png
62
+ ```
63
+
64
+ #### 간편 CLI (genthumb)
65
+ ```bash
66
+ # 기본
67
+ uv run genthumb
68
+
69
+ # 제목/부제목 덮어쓰기
70
+ uv run genthumb --title "새 제목" --subtitle "새 부제목"
71
+
72
+ # 배경 이미지 설정
73
+ uv run genthumb --bgImg bg.png
74
+
75
+ # 출력 파일 지정
76
+ uv run genthumb -o result.png
77
+ ```
78
+
79
+ ## 파일 구조
80
+
81
+ ```
82
+ thumbnail_maker/
83
+ ├── requirements.txt # Python 패키지 의존성
84
+ ├── thumbnailRenderer.py # 핵심 렌더링 로직
85
+ ├── generateThumbnail.py # 메인 생성 스크립트
86
+ ├── genthumb.py # 간편 CLI 스크립트
87
+ ├── main_gui.py # PySide6 GUI 애플리케이션
88
+ └── thumbnail.json # DSL 예제 파일
89
+ ```
90
+
91
+ ## DSL 파일 형식
92
+
93
+ ```json
94
+ {
95
+ "Thumbnail": {
96
+ "Resolution": {
97
+ "type": "preset",
98
+ "value": "16:9"
99
+ },
100
+ "Background": {
101
+ "type": "solid",
102
+ "color": "#a3e635"
103
+ },
104
+ "Texts": [
105
+ {
106
+ "type": "title",
107
+ "content": "제목 텍스트",
108
+ "gridPosition": "tl",
109
+ "font": {
110
+ "name": "SBAggroB",
111
+ "faces": [...]
112
+ },
113
+ "fontSize": 48,
114
+ "color": "#4ade80",
115
+ "outline": {
116
+ "thickness": 7,
117
+ "color": "#000000"
118
+ },
119
+ "enabled": true
120
+ }
121
+ ]
122
+ }
123
+ }
124
+ ```
125
+
126
+ ## 해상도 설정
127
+
128
+ ### Preset 모드
129
+ ```json
130
+ {
131
+ "type": "preset",
132
+ "value": "16:9" // "16:9", "9:16", "4:3", "1:1"
133
+ }
134
+ ```
135
+
136
+ ### Fixed Ratio 모드
137
+ ```json
138
+ {
139
+ "type": "fixedRatio",
140
+ "ratioValue": "16:9",
141
+ "width": 480 // 또는 height 지정
142
+ }
143
+ ```
144
+
145
+ ### Custom 모드
146
+ ```json
147
+ {
148
+ "type": "custom",
149
+ "width": 480,
150
+ "height": 270
151
+ }
152
+ ```
153
+
154
+ ## 기타
155
+
156
+ - JavaScript 버전의 파일들은 유지됩니다.
157
+ - 기존 DSL 파일과 호환됩니다.
158
+ - 폰트는 `fonts/` 디렉토리에 저장됩니다.
159
+
@@ -0,0 +1,16 @@
1
+ thumbnail_maker/__init__.py,sha256=ybcxf-LnaQ2LA3lf9oiDPuQ0ggIoeX2D0U07TF-dJGk,144
2
+ thumbnail_maker/__main__.py,sha256=TbnhYzU8y-IsEq2c1Vt2pnRLkP_vjO6yCHM1f6_bKZo,4535
3
+ thumbnail_maker/cli.py,sha256=bH1bSjhPuvGNkHe3kF3stNQ7ujBU07b1dyPspoMWBgY,5256
4
+ thumbnail_maker/renderer.py,sha256=wNmooCpdZ45_susu9zIChlalho-ubSI0q3iXrkJQRLE,25142
5
+ thumbnail_maker/upload.py,sha256=5uYqql5OyZc27D35QQaxHn2_363q8AA3pa74Ph-eZvc,9454
6
+ thumbnail_maker/gui/__init__.py,sha256=ZnX03kIRCm5BdNfua2TVqs6z1NXz0bDaO1H6Qq7t7y0,170
7
+ thumbnail_maker/gui/dsl_manager.py,sha256=vd5hskxDyQ4YjsYR3mfsbCCHqTwh5usCU2IKWX8bpn8,15385
8
+ thumbnail_maker/gui/font_utils.py,sha256=qGuw-4AtvysOy3M1URQcwg1j85MbHVb5gXxs79b6QM8,1213
9
+ thumbnail_maker/gui/handlers.py,sha256=d-v-Yv2k59ImQ94B7I37_0km2of1tEGErHBM9n9zf2I,11559
10
+ thumbnail_maker/gui/main_window.py,sha256=RwHujw-kjYIhcdFIOEyUysYPfvWphBYbdtMbWnT7z3Q,6000
11
+ thumbnail_maker/gui/preview_thread.py,sha256=b5LaZsMujXwuEKftzEvwqzlOyqOaCBV_yvd04nExGyg,753
12
+ thumbnail_maker/gui/widgets.py,sha256=G81t46aRL2gHYr-EXhE2x1dWf_81HOLkGETVybLsvNE,16433
13
+ thumbnail_maker-0.1.6.dist-info/METADATA,sha256=YrK7BYAl1RK8W3QrrhrGL0DpDt4rdUUYcTdspAbQwN8,2937
14
+ thumbnail_maker-0.1.6.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
15
+ thumbnail_maker-0.1.6.dist-info/entry_points.txt,sha256=rqhlHR3PzlOlRmCL745NshLfST4yxtGNubqAghAJ5hA,66
16
+ thumbnail_maker-0.1.6.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ thumbnail_maker = thumbnail_maker.__main__:main