tomo2chin2 commited on
Commit
94445ba
·
verified ·
1 Parent(s): 5067c93

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +149 -37
app.py CHANGED
@@ -1,3 +1,5 @@
 
 
1
  import gradio as gr
2
  from selenium import webdriver
3
  from selenium.webdriver.chrome.options import Options
@@ -9,87 +11,197 @@ from io import BytesIO
9
  import tempfile
10
  import time
11
  import os
 
 
 
 
 
 
 
 
12
 
13
- def render_fullpage_screenshot(html_code, extension_percentage):
 
 
14
  """
15
  html_code: HTMLのソースコード文字列
16
  extension_percentage: 上下に追加する余裕の% (例: 4なら合計4%の余裕)
 
17
  """
18
- # 1) HTMLコードを一時ファイルに保存
19
- tmp_file = tempfile.NamedTemporaryFile(suffix=".html", delete=False)
20
- tmp_path = tmp_file.name
21
- tmp_file.write(html_code.encode('utf-8'))
22
- tmp_file.close()
23
-
24
- # 2) ヘッドレスChrome(Chromium)起動オプション
25
- options = Options()
26
- options.add_argument("--headless")
27
- options.add_argument("--no-sandbox")
28
- options.add_argument("--disable-dev-shm-usage")
29
- options.add_argument("--force-device-scale-factor=1")
30
 
31
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  driver = webdriver.Chrome(options=options)
 
33
 
34
- # 3) 初期ウィンドウサイズでページを読み込む
35
- driver.set_window_size(1200, 800)
36
- driver.get("file://" + tmp_path)
 
 
37
 
38
- # 4) ページのロードを待機
39
- WebDriverWait(driver, 10).until(
 
40
  EC.presence_of_element_located((By.TAG_NAME, "body"))
41
  )
42
- time.sleep(2) # 外部リソースの読み込み待機
 
43
 
44
  # 5) スクロールバーがキャプチャに写らないようCSSで非表示
 
45
  driver.execute_script(
46
  "document.documentElement.style.overflow = 'hidden';"
47
  "document.body.style.overflow = 'hidden';"
48
  )
49
 
50
- # 6) ページ全体の幅・高さを正確に取得 (bodyとdocumentElementの両方から)
 
51
  scroll_width = driver.execute_script(
52
- "return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth)"
53
  )
54
  scroll_height = driver.execute_script(
55
- "return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight)"
56
  )
 
 
 
 
 
 
57
 
58
  # 7) 上下方向にだけユーザー指定の余裕(%)を加えた高さを計算
59
- adjusted_height = int(scroll_height * (1 + extension_percentage/100))
60
-
 
61
  # 8) ウィンドウサイズを、幅はそのまま、縦はadjusted_heightに変更
 
 
 
62
  driver.set_window_size(scroll_width, adjusted_height)
63
- time.sleep(2) # レイアウトの安定化待機
 
64
 
65
  # 念のため最上部にスクロール
 
66
  driver.execute_script("window.scrollTo(0, 0)")
67
  time.sleep(1)
68
 
69
  # 9) スクリーンショット取得
 
70
  png = driver.get_screenshot_as_png()
 
 
 
71
 
72
  except Exception as e:
 
73
  # エラー時は1x1の黒画像を返す
74
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
75
  finally:
76
- driver.quit()
77
- if os.path.exists(tmp_path):
78
- os.remove(tmp_path)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
 
80
- return Image.open(BytesIO(png))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- # Gradioインターフェースの定義
83
- iface = gr.Interface(
84
- fn=render_fullpage_screenshot,
 
 
 
 
 
 
 
85
  inputs=[
86
  gr.Textbox(lines=15, label="HTMLコード入力"),
87
- gr.Slider(minimum=0, maximum=20, step=1.0, value=8, label="上下高さ拡張率(%)")
88
  ],
89
  outputs=gr.Image(type="pil", label="ページ全体のスクリーンショット"),
90
- title="Full Page Screenshot API (高さ拡張調整可能)",
91
- description="HTMLをヘッドレスブラウザでレンダリングし、ページ全体を1枚の画像として取得します。上下のみユーザー指定の余裕(%)を追加します。"
 
92
  )
93
 
94
- if __name__ == "__main__":
95
- iface.launch()
 
 
 
 
 
 
 
 
 
1
+ # --- START OF FILE app.py ---
2
+
3
  import gradio as gr
4
  from selenium import webdriver
5
  from selenium.webdriver.chrome.options import Options
 
11
  import tempfile
12
  import time
13
  import os
14
+ from fastapi import FastAPI, HTTPException, Request
15
+ from fastapi.responses import StreamingResponse
16
+ from pydantic import BaseModel, Field
17
+ import logging
18
+
19
+ # ロギング設定 (デバッグに役立つ)
20
+ logging.basicConfig(level=logging.INFO)
21
+ logger = logging.getLogger(__name__)
22
 
23
+ # --- コアロジック: スクリーンショット生成関数 ---
24
+ # (元のコードから変更なし、エラーハンドリングを少し明確化)
25
+ def render_fullpage_screenshot(html_code: str, extension_percentage: float) -> Image.Image:
26
  """
27
  html_code: HTMLのソースコード文字列
28
  extension_percentage: 上下に追加する余裕の% (例: 4なら合計4%の余裕)
29
+ 戻り値: PIL Imageオブジェクト。エラー時は1x1の黒画像。
30
  """
31
+ tmp_file = None
32
+ driver = None
33
+ tmp_path = ""
 
 
 
 
 
 
 
 
 
34
 
35
  try:
36
+ # 1) HTMLコードを一時ファイルに保存
37
+ # delete=False と NamedTemporaryFile の close/remove の組み合わせはLinux/Windowsで挙動が安定しないことがあるため、
38
+ # with構文と手動削除を確実に組み合わせる
39
+ with tempfile.NamedTemporaryFile(suffix=".html", delete=False, mode='w', encoding='utf-8') as tmp_file:
40
+ tmp_path = tmp_file.name
41
+ tmp_file.write(html_code)
42
+ logger.info(f"HTML saved to temporary file: {tmp_path}")
43
+
44
+ # 2) ヘッドレスChrome(Chromium)起動オプション
45
+ options = Options()
46
+ options.add_argument("--headless")
47
+ options.add_argument("--no-sandbox")
48
+ options.add_argument("--disable-dev-shm-usage") # メモリ不足エラー対策
49
+ options.add_argument("--force-device-scale-factor=1") # DPIスケーリング無効化
50
+ options.add_argument("--window-size=1200,800") # 初期ウィンドウサイズ
51
+ # デバッグ用にログレベルを上げる (オプション)
52
+ # options.add_argument("--enable-logging")
53
+ # options.add_argument("--v=1")
54
+
55
+ logger.info("Initializing WebDriver...")
56
+ # サービスオブジェクトを作成してログ出力を制御 (オプション)
57
+ # service = webdriver.chrome.service.Service(log_output=subprocess.STDOUT)
58
+ # driver = webdriver.Chrome(options=options, service=service)
59
  driver = webdriver.Chrome(options=options)
60
+ logger.info("WebDriver initialized.")
61
 
62
+ # 3) 初期ウィンドウサイズでページを読み込む (オプションで設定済みなので不要かも)
63
+ # driver.set_window_size(1200, 800)
64
+ file_url = "file://" + tmp_path
65
+ logger.info(f"Navigating to {file_url}")
66
+ driver.get(file_url)
67
 
68
+ # 4) ページのロードを待機 (body要素の存在確認)
69
+ logger.info("Waiting for body element...")
70
+ WebDriverWait(driver, 20).until( # タイムアウトを少し延長
71
  EC.presence_of_element_located((By.TAG_NAME, "body"))
72
  )
73
+ logger.info("Body element found. Waiting for potential dynamic content loading...")
74
+ time.sleep(3) # 外部リソースやJSによる動的変更の待機時間を少し増やす
75
 
76
  # 5) スクロールバーがキャプチャに写らないようCSSで非表示
77
+ logger.info("Hiding scrollbars via JS...")
78
  driver.execute_script(
79
  "document.documentElement.style.overflow = 'hidden';"
80
  "document.body.style.overflow = 'hidden';"
81
  )
82
 
83
+ # 6) ページ全体の幅・高さを正確に取得
84
+ logger.info("Calculating page dimensions...")
85
  scroll_width = driver.execute_script(
86
+ "return Math.max(document.body.scrollWidth, document.documentElement.scrollWidth, document.body.offsetWidth, document.documentElement.offsetWidth)"
87
  )
88
  scroll_height = driver.execute_script(
89
+ "return Math.max(document.body.scrollHeight, document.documentElement.scrollHeight, document.body.offsetHeight, document.documentElement.offsetHeight)"
90
  )
91
+ logger.info(f"Calculated dimensions: width={scroll_width}, height={scroll_height}")
92
+
93
+ # 幅が0や極端に小さい場合はデフォルト値を使うなど対策が必要になる可能性あり
94
+ if scroll_width < 100:
95
+ logger.warning(f"Detected very small scroll_width ({scroll_width}), defaulting to 1200")
96
+ scroll_width = 1200
97
 
98
  # 7) 上下方向にだけユーザー指定の余裕(%)を加えた高さを計算
99
+ adjusted_height = int(scroll_height * (1 + extension_percentage / 100))
100
+ logger.info(f"Adjusted height: {adjusted_height} (extension: {extension_percentage}%)")
101
+
102
  # 8) ウィンドウサイズを、幅はそのまま、縦はadjusted_heightに変更
103
+ logger.info(f"Resizing window to {scroll_width} x {adjusted_height}")
104
+ # set_window_sizeが期待通りに動作しない場合があるため、最大化してからサイズ変更を試すことも検討
105
+ # driver.maximize_window()
106
  driver.set_window_size(scroll_width, adjusted_height)
107
+ logger.info("Waiting for layout stabilization after resize...")
108
+ time.sleep(3) # レイアウトの安定化待機時間を少し増やす
109
 
110
  # 念のため最上部にスクロール
111
+ logger.info("Scrolling to top...")
112
  driver.execute_script("window.scrollTo(0, 0)")
113
  time.sleep(1)
114
 
115
  # 9) スクリーンショット取得
116
+ logger.info("Taking screenshot...")
117
  png = driver.get_screenshot_as_png()
118
+ logger.info("Screenshot taken successfully.")
119
+
120
+ return Image.open(BytesIO(png))
121
 
122
  except Exception as e:
123
+ logger.error(f"Error during screenshot generation: {e}", exc_info=True)
124
  # エラー時は1x1の黒画像を返す
125
  return Image.new('RGB', (1, 1), color=(0, 0, 0))
126
  finally:
127
+ # 必ず WebDriver を終了する
128
+ if driver:
129
+ logger.info("Quitting WebDriver...")
130
+ driver.quit()
131
+ logger.info("WebDriver quit.")
132
+ # 一時ファイルを確実に削除する
133
+ if tmp_path and os.path.exists(tmp_path):
134
+ try:
135
+ os.remove(tmp_path)
136
+ logger.info(f"Temporary file removed: {tmp_path}")
137
+ except OSError as e:
138
+ logger.error(f"Error removing temporary file {tmp_path}: {e}")
139
+
140
+ # --- FastAPI アプリケーション設定 ---
141
+ app = FastAPI(
142
+ title="Full Page Screenshot API",
143
+ description="Renders HTML using a headless browser and returns a full-page screenshot.",
144
+ version="1.0.0"
145
+ )
146
+
147
+ # --- APIリクエストモデル ---
148
+ class ScreenshotRequest(BaseModel):
149
+ html_code: str = Field(..., description="HTML source code to render.")
150
+ extension_percentage: float = Field(8.0, ge=0, le=100, description="Percentage of extra height to add (top and bottom combined). Default: 8%")
151
 
152
+ # --- APIエンドポイント ---
153
+ @app.post("/screenshot",
154
+ response_class=StreamingResponse,
155
+ tags=["Screenshot"],
156
+ summary="Generate Full Page Screenshot",
157
+ description="Takes HTML code and returns a PNG image of the rendered full page.")
158
+ async def create_screenshot(request: ScreenshotRequest):
159
+ """
160
+ Generates a full-page screenshot from the provided HTML code.
161
+ """
162
+ logger.info(f"Received API request. HTML length: {len(request.html_code)}, Extension: {request.extension_percentage}%")
163
+ try:
164
+ pil_image = render_fullpage_screenshot(request.html_code, request.extension_percentage)
165
+
166
+ # エラー画像 (1x1) が返ってきたかチェック
167
+ if pil_image.size == (1, 1):
168
+ logger.error("Screenshot generation failed, returning 500 error.")
169
+ # エラーを示すために 500 Internal Server Error を返すことも検討できる
170
+ # raise HTTPException(status_code=500, detail="Failed to generate screenshot. Check HTML or server logs.")
171
+ # ここでは仕様通り 1x1 画像を返すことにするが、APIとしてはエラーの方が親切かもしれない
172
+
173
+ # PIL画像をPNGバイトデータに変換
174
+ img_byte_arr = BytesIO()
175
+ pil_image.save(img_byte_arr, format='PNG')
176
+ img_byte_arr.seek(0) # ポインタを先頭に戻す
177
 
178
+ logger.info("Returning screenshot as PNG stream.")
179
+ return StreamingResponse(img_byte_arr, media_type="image/png")
180
+
181
+ except Exception as e:
182
+ logger.error(f"Unhandled exception in API endpoint: {e}", exc_info=True)
183
+ raise HTTPException(status_code=500, detail=f"Internal server error: {e}")
184
+
185
+ # --- Gradioインターフェース定義 ---
186
+ gradio_interface = gr.Interface(
187
+ fn=render_fullpage_screenshot, # コアロジック関数を再利用
188
  inputs=[
189
  gr.Textbox(lines=15, label="HTMLコード入力"),
190
+ gr.Slider(minimum=0, maximum=30, step=1.0, value=8, label="上下高さ拡張率(%)") # 最大値を少し増やす
191
  ],
192
  outputs=gr.Image(type="pil", label="ページ全体のスクリーンショット"),
193
+ title="Full Page Screenshot (高さ拡張調整可能)",
194
+ description="HTMLをヘッドレスブラウザでレンダリングし、ページ全体を1枚の画像として取得します。上下のみユーザー指定の余裕(%)を追加します。APIは /screenshot (POST) で利用可能です。",
195
+ allow_flagging="never" # Hugging Face Spacesでフラグ機能を無効化
196
  )
197
 
198
+ # --- GradioアプリをFastAPIにマウント ---
199
+ # ルートパス ("/") でGradioインターフェースを提供
200
+ app = gr.mount_gradio_app(app, gradio_interface, path="/")
201
+
202
+ # --- uvicorn で実行するための設定 (Hugging Face Spacesでは通常不要) ---
203
+ # if __name__ == "__main__":
204
+ # import uvicorn
205
+ # uvicorn.run(app, host="0.0.0.0", port=7860)
206
+
207
+ # --- END OF FILE app.py ---