99i commited on
Commit
8c0a03b
·
verified ·
1 Parent(s): 604b1fa

Upload 3 files

Browse files
Files changed (3) hide show
  1. Dockerfile +35 -0
  2. main.py +361 -0
  3. requirements.txt +3 -0
Dockerfile ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 使用官方Python运行时作为基础镜像
2
+ FROM python:3.10-slim
3
+
4
+ # 设置工作目录
5
+ WORKDIR /app
6
+
7
+ # 安装系统依赖
8
+ RUN apt-get update && apt-get install -y \
9
+ wget \
10
+ gnupg \
11
+ && rm -rf /var/lib/apt/lists/*
12
+
13
+ # 安装Chromium浏览器(DrissionPage需要)
14
+ RUN apt-get update && apt-get install -y \
15
+ chromium \
16
+ chromium-driver \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # 复制依赖文件
20
+ COPY requirements.txt .
21
+
22
+ # 安装Python依赖
23
+ RUN pip install --no-cache-dir -r requirements.txt
24
+
25
+ # 复制应用代码
26
+ COPY . .
27
+
28
+ # 创建临时目录用于存储图片
29
+ RUN mkdir -p temp
30
+
31
+ # 暴露端口
32
+ EXPOSE 8000
33
+
34
+ # 启动应用
35
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
main.py ADDED
@@ -0,0 +1,361 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Request
2
+ from fastapi.responses import HTMLResponse, FileResponse
3
+ from fastapi.staticfiles import StaticFiles
4
+ from pydantic import BaseModel
5
+ from typing import Optional
6
+ import asyncio
7
+ import tempfile
8
+ import os
9
+ import uuid
10
+ from drivisionpage import ChromiumPage
11
+
12
+ app = FastAPI(title="HTML to Image Converter", version="1.0.0")
13
+
14
+ # 挂载静态文件目录
15
+ app.mount("/static", StaticFiles(directory="static"), name="static")
16
+
17
+ # 创建临时目录用于存储图片
18
+ os.makedirs("temp", exist_ok=True)
19
+
20
+ class ImageRequest(BaseModel):
21
+ url: Optional[str] = None
22
+ html_content: Optional[str] = None
23
+ selector: Optional[str] = None
24
+ full_page: bool = False
25
+ width: int = 1200
26
+ height: int = 800
27
+
28
+ class ImageResponse(BaseModel):
29
+ success: bool
30
+ image_url: Optional[str] = None
31
+ error: Optional[str] = None
32
+
33
+ @app.get("/", response_class=HTMLResponse)
34
+ async def read_root(request: Request):
35
+ return """
36
+ <!DOCTYPE html>
37
+ <html>
38
+ <head>
39
+ <title>HTML to Image Converter</title>
40
+ <meta charset="UTF-8">
41
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
+ <style>
43
+ body {
44
+ font-family: Arial, sans-serif;
45
+ max-width: 800px;
46
+ margin: 0 auto;
47
+ padding: 20px;
48
+ background-color: #f5f5f5;
49
+ }
50
+ .container {
51
+ background-color: white;
52
+ padding: 30px;
53
+ border-radius: 10px;
54
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
55
+ }
56
+ h1 {
57
+ color: #333;
58
+ text-align: center;
59
+ }
60
+ .form-group {
61
+ margin-bottom: 20px;
62
+ }
63
+ label {
64
+ display: block;
65
+ margin-bottom: 5px;
66
+ font-weight: bold;
67
+ }
68
+ input, textarea, select {
69
+ width: 100%;
70
+ padding: 10px;
71
+ border: 1px solid #ddd;
72
+ border-radius: 5px;
73
+ box-sizing: border-box;
74
+ }
75
+ button {
76
+ background-color: #4CAF50;
77
+ color: white;
78
+ padding: 12px 20px;
79
+ border: none;
80
+ border-radius: 5px;
81
+ cursor: pointer;
82
+ font-size: 16px;
83
+ width: 100%;
84
+ }
85
+ button:hover {
86
+ background-color: #45a049;
87
+ }
88
+ #result {
89
+ margin-top: 20px;
90
+ text-align: center;
91
+ }
92
+ #imagePreview {
93
+ max-width: 100%;
94
+ border: 1px solid #ddd;
95
+ border-radius: 5px;
96
+ margin-top: 10px;
97
+ }
98
+ .tab {
99
+ overflow: hidden;
100
+ border: 1px solid #ccc;
101
+ background-color: #f1f1f1;
102
+ }
103
+ .tab button {
104
+ background-color: inherit;
105
+ float: left;
106
+ border: none;
107
+ outline: none;
108
+ cursor: pointer;
109
+ padding: 14px 16px;
110
+ transition: 0.3s;
111
+ font-size: 17px;
112
+ }
113
+ .tab button:hover {
114
+ background-color: #ddd;
115
+ }
116
+ .tab button.active {
117
+ background-color: #ccc;
118
+ }
119
+ .tabcontent {
120
+ display: none;
121
+ padding: 6px 12px;
122
+ border: 1px solid #ccc;
123
+ border-top: none;
124
+ }
125
+ </style>
126
+ </head>
127
+ <body>
128
+ <div class="container">
129
+ <h1>HTML to Image Converter</h1>
130
+
131
+ <div class="tab">
132
+ <button class="tablinks active" onclick="openTab(event, 'urlTab')">URL地址</button>
133
+ <button class="tablinks" onclick="openTab(event, 'htmlTab')">HTML代码</button>
134
+ </div>
135
+
136
+ <div id="urlTab" class="tabcontent" style="display: block;">
137
+ <form id="imageFormUrl">
138
+ <div class="form-group">
139
+ <label for="url">网页URL:</label>
140
+ <input type="url" id="url" name="url" required placeholder="https://example.com">
141
+ </div>
142
+ <div class="form-group">
143
+ <label for="selector">CSS选择器 (可选):</label>
144
+ <input type="text" id="selector" name="selector" placeholder="例如: .content, #main">
145
+ </div>
146
+ <div class="form-group">
147
+ <label for="full_page">截图类型:</label>
148
+ <select id="full_page" name="full_page">
149
+ <option value="false">可见区域</option>
150
+ <option value="true">完整页面</option>
151
+ </select>
152
+ </div>
153
+ <div class="form-group">
154
+ <label for="width">宽度:</label>
155
+ <input type="number" id="width" name="width" value="1200" min="100" max="5000">
156
+ </div>
157
+ <div class="form-group">
158
+ <label for="height">高度:</label>
159
+ <input type="number" id="height" name="height" value="800" min="100" max="5000">
160
+ </div>
161
+ <button type="submit">生成图片</button>
162
+ </form>
163
+ </div>
164
+
165
+ <div id="htmlTab" class="tabcontent">
166
+ <form id="imageFormHtml">
167
+ <div class="form-group">
168
+ <label for="html_content">HTML代码:</label>
169
+ <textarea id="html_content" name="html_content" rows="10" placeholder="在这里输入HTML代码..."></textarea>
170
+ </div>
171
+ <div class="form-group">
172
+ <label for="selectorHtml">CSS选择器 (可选):</label>
173
+ <input type="text" id="selectorHtml" name="selector" placeholder="例如: .content, #main">
174
+ </div>
175
+ <div class="form-group">
176
+ <label for="full_page_html">截图类型:</label>
177
+ <select id="full_page_html" name="full_page">
178
+ <option value="false">可见区域</option>
179
+ <option value="true">完整页面</option>
180
+ </select>
181
+ </div>
182
+ <div class="form-group">
183
+ <label for="width_html">宽度:</label>
184
+ <input type="number" id="width_html" name="width" value="1200" min="100" max="5000">
185
+ </div>
186
+ <div class="form-group">
187
+ <label for="height_html">高度:</label>
188
+ <input type="number" id="height_html" name="height" value="800" min="100" max="5000">
189
+ </div>
190
+ <button type="submit">生成图片</button>
191
+ </form>
192
+ </div>
193
+
194
+ <div id="result"></div>
195
+ </div>
196
+
197
+ <script>
198
+ // 切换标签页
199
+ function openTab(evt, tabName) {
200
+ var i, tabcontent, tablinks;
201
+ tabcontent = document.querySelectorAll(".tabcontent");
202
+ for (i = 0; i < tabcontent.length; i++) {
203
+ tabcontent[i].style.display = "none";
204
+ }
205
+ tablinks = document.querySelectorAll(".tablinks");
206
+ for (i = 0; i < tablinks.length; i++) {
207
+ tablinks[i].className = tablinks[i].className.replace(" active", "");
208
+ }
209
+ document.getElementById(tabName).style.display = "block";
210
+ evt.currentTarget.className += " active";
211
+ }
212
+
213
+ // URL表单提交处理
214
+ document.getElementById('imageFormUrl').addEventListener('submit', async function(e) {
215
+ e.preventDefault();
216
+ const resultDiv = document.getElementById('result');
217
+ resultDiv.innerHTML = '<p>正在处理...</p>';
218
+
219
+ const formData = new FormData(this);
220
+ const data = Object.fromEntries(formData);
221
+ data['html_content'] = null; // 清除HTML内容
222
+
223
+ try {
224
+ const response = await fetch('/convert', {
225
+ method: 'POST',
226
+ headers: {
227
+ 'Content-Type': 'application/json',
228
+ },
229
+ body: JSON.stringify(data)
230
+ });
231
+
232
+ const result = await response.json();
233
+
234
+ if (result.success) {
235
+ resultDiv.innerHTML = `
236
+ <h2>转换成功!</h2>
237
+ <img id="imagePreview" src="${result.image_url}" alt="生成的图片">
238
+ `;
239
+ } else {
240
+ resultDiv.innerHTML = `<p style="color: red;">错误: ${result.error}</p>`;
241
+ }
242
+ } catch (error) {
243
+ resultDiv.innerHTML = `<p style="color: red;">请求失败: ${error.message}</p>`;
244
+ }
245
+ });
246
+
247
+ // HTML表单提交处理
248
+ document.getElementById('imageFormHtml').addEventListener('submit', async function(e) {
249
+ e.preventDefault();
250
+ const resultDiv = document.getElementById('result');
251
+ resultDiv.innerHTML = '<p>正在处理...</p>';
252
+
253
+ const formData = new FormData(this);
254
+ const data = Object.fromEntries(formData);
255
+ data['url'] = null; // 清除URL
256
+
257
+ try {
258
+ const response = await fetch('/convert', {
259
+ method: 'POST',
260
+ headers: {
261
+ 'Content-Type': 'application/json',
262
+ },
263
+ body: JSON.stringify(data)
264
+ });
265
+
266
+ const result = await response.json();
267
+
268
+ if (result.success) {
269
+ resultDiv.innerHTML = `
270
+ <h2>转换成功!</h2>
271
+ <img id="imagePreview" src="${result.image_url}" alt="生成的图片">
272
+ `;
273
+ } else {
274
+ resultDiv.innerHTML = `<p style="color: red;">错误: ${result.error}</p>`;
275
+ }
276
+ } catch (error) {
277
+ resultDiv.innerHTML = `<p style="color: red;">请求失败: ${error.message}</p>`;
278
+ }
279
+ });
280
+ </script>
281
+ </body>
282
+ </html>
283
+ """
284
+
285
+ @app.post("/convert", response_model=ImageResponse)
286
+ async def convert_html_to_image(request: ImageRequest):
287
+ try:
288
+ # 生成唯一的临时文件名
289
+ unique_id = str(uuid.uuid4())
290
+ temp_image_path = f"temp/{unique_id}.png"
291
+
292
+ # 使用DrissionPage进行截图
293
+ page = ChromiumPage()
294
+
295
+ try:
296
+ # 设置页面视口大小
297
+ page.set.window.size(request.width, request.height)
298
+
299
+ # 根据是URL还是HTML内容来处理
300
+ if request.url:
301
+ # 打开URL
302
+ page.get(request.url)
303
+
304
+ # 等待页面加载
305
+ page.wait.loadstart()
306
+ page.wait.loadfinish()
307
+ elif request.html_content:
308
+ # 使用HTML内容
309
+ page.get("about:blank")
310
+ page.ele("body").html(request.html_content)
311
+ # 等待页面渲染完成
312
+ page.wait.loadfinish()
313
+ else:
314
+ raise HTTPException(status_code=400, detail="必须提供URL或HTML内容")
315
+
316
+ # 根据参数决定截图方式
317
+ if request.selector:
318
+ element = page.ele(request.selector)
319
+ if element:
320
+ element.screenshot(path=temp_image_path)
321
+ else:
322
+ raise HTTPException(status_code=400, detail="CSS选择器未找到元素")
323
+ elif request.full_page:
324
+ page.screenshot(path=temp_image_path, full_page=True)
325
+ else:
326
+ page.screenshot(path=temp_image_path)
327
+
328
+ finally:
329
+ page.quit()
330
+
331
+ return ImageResponse(
332
+ success=True,
333
+ image_url=f"/{temp_image_path}"
334
+ )
335
+
336
+ except Exception as e:
337
+ return ImageResponse(
338
+ success=False,
339
+ error=str(e)
340
+ )
341
+
342
+ @app.get("/{image_path:path}")
343
+ async def get_image(image_path: str):
344
+ # 安全检查,只允许访问temp目录下的文件
345
+ if not image_path.startswith("temp/"):
346
+ raise HTTPException(status_code=404, detail="Image not found")
347
+
348
+ # 检查文件是否存在
349
+ if not os.path.exists(image_path):
350
+ raise HTTPException(status_code=404, detail="Image not found")
351
+
352
+ # 返回图片文件
353
+ return FileResponse(image_path)
354
+
355
+ @app.get("/health")
356
+ async def health_check():
357
+ return {"status": "healthy"}
358
+
359
+ if __name__ == "__main__":
360
+ import uvicorn
361
+ uvicorn.run(app, host="0.0.0.0", port=8000)
requirements.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ fastapi==0.104.1
2
+ uvicorn==0.24.0
3
+ drissionpage==4.1.1