Spaces:
Paused
Paused
| import os | |
| import math | |
| import time | |
| import tempfile | |
| import gc | |
| from io import BytesIO | |
| import gradio as gr | |
| from PIL import Image | |
| from selenium import webdriver | |
| from selenium.webdriver.chrome.options import Options | |
| from selenium.webdriver.common.by import By | |
| from selenium.webdriver.support.ui import WebDriverWait | |
| from selenium.webdriver.support import expected_conditions as EC | |
| def setup_driver(): | |
| """ | |
| ヘッドレスChromiumブラウザを起動するためのWebDriverを初期化する。 | |
| Hugging Face Spacesで動かす場合は、packages.txtにchromium-driverが必要。 | |
| """ | |
| options = Options() | |
| options.add_argument("--headless") | |
| options.add_argument("--no-sandbox") | |
| options.add_argument("--disable-dev-shm-usage") | |
| options.add_argument("--force-device-scale-factor=1") | |
| try: | |
| driver = webdriver.Chrome(options=options) | |
| # スタート時の画面サイズを固定しておく | |
| driver.set_window_size(1280, 800) | |
| return driver | |
| except Exception as e: | |
| print(f"WebDriver初期化エラー: {e}") | |
| return None | |
| def calculate_scroll_parameters(driver): | |
| """ | |
| スクロールに必要なパラメータを精密に計算する。 | |
| """ | |
| # ビューポートの高さ(表示領域) | |
| viewport_height = driver.execute_script("return window.innerHeight") | |
| # ページ全体の高さ | |
| total_height = driver.execute_script(""" | |
| return Math.max( | |
| document.body.scrollHeight, | |
| document.documentElement.scrollHeight | |
| ); | |
| """) | |
| # スクロール回数を算出(端数が出たら繰り上げ) | |
| scroll_steps = math.ceil(total_height / viewport_height) | |
| # 画像の重複を少し入れておく(50px程度)と、つなぎ目がズレにくい | |
| overlap_pixels = 50 | |
| return viewport_height, total_height, scroll_steps, overlap_pixels | |
| def process_screenshot_tiles(tiles, viewport_height, total_height, overlap): | |
| """ | |
| 複数のタイル画像(tiles)を縦に合成して1枚のフルページ画像を作る。 | |
| overlap: タイル同士を結合する際に重複させる領域(px) | |
| """ | |
| if not tiles: | |
| return None | |
| # 合成画像の最終サイズ | |
| # 幅 = 最初のタイルの幅, 高さ = ページ全体の高さ | |
| stitched_width = tiles[0].width | |
| stitched_height = total_height | |
| # 新しい画像を作る | |
| stitched = Image.new('RGB', (stitched_width, stitched_height)) | |
| y_offset = 0 | |
| for i, tile in enumerate(tiles): | |
| # タイルを貼り付ける | |
| stitched.paste(tile, (0, y_offset)) | |
| # 最終タイル以外はオーバーラップ分を差し引いて次の貼り付け位置を計算 | |
| if i < len(tiles) - 1: | |
| y_offset += tile.height - overlap | |
| else: | |
| # 最終タイルではオーバーラップを引かないで終了 | |
| y_offset += tile.height | |
| return stitched | |
| def capture_fullpage_screenshot(driver, html_content, css_content): | |
| """ | |
| 与えられたHTMLコードとCSSコードを結合したページを生成し、 | |
| ヘッドレスブラウザでフルページスクリーンショットを取得(スクロール結合)。 | |
| """ | |
| temp_path = None | |
| try: | |
| # もし不要ならCSS非表示でもOK | |
| # driver.execute_script("document.querySelector('style').style.display='none';") | |
| # 一時的なHTMLファイルを作る | |
| with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as f: | |
| f.write(f""" | |
| <html> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <style> | |
| {css_content} | |
| </style> | |
| </head> | |
| <body> | |
| {html_content} | |
| </body> | |
| </html> | |
| """.encode('utf-8')) | |
| temp_path = f.name | |
| # ローカルHTMLを読み込み | |
| driver.get(f"file://{temp_path}") | |
| # ページロード完了を待機 | |
| WebDriverWait(driver, 10).until( | |
| EC.presence_of_element_located((By.TAG_NAME, "body")) | |
| ) | |
| time.sleep(1.0) # 追加で待機(フォントや画像の読み込み) | |
| # スクロールパラメータ計算 | |
| viewport_height, total_height, scroll_steps, overlap_pixels = calculate_scroll_parameters(driver) | |
| tiles = [] | |
| for step in range(scroll_steps): | |
| scroll_y = step * (viewport_height - overlap_pixels) | |
| driver.execute_script(f"window.scrollTo(0, {scroll_y});") | |
| # DOMが再描画されるまで待機 | |
| time.sleep(0.4) | |
| # スクリーンショット撮影 | |
| screenshot = driver.get_screenshot_as_png() | |
| tile_img = Image.open(BytesIO(screenshot)) | |
| tiles.append(tile_img) | |
| # タイルを結合 | |
| full_image = process_screenshot_tiles(tiles, viewport_height, total_height, overlap_pixels) | |
| # メモリ解放(念のため) | |
| del tiles | |
| gc.collect() | |
| return full_image, None | |
| except Exception as e: | |
| return None, f"エラー発生: {str(e)}" | |
| finally: | |
| if temp_path and os.path.exists(temp_path): | |
| os.unlink(temp_path) | |
| def gradio_interface(html_input, css_input): | |
| """ | |
| Gradioから呼び出すメイン関数。 | |
| """ | |
| driver = setup_driver() | |
| if not driver: | |
| return None, "WebDriver初期化に失敗しました。" | |
| image, error = capture_fullpage_screenshot(driver, html_input, css_input) | |
| driver.quit() | |
| if error: | |
| return None, error | |
| else: | |
| return image, None | |
| # Gradioアプリの定義 | |
| iface = gr.Interface( | |
| fn=gradio_interface, | |
| inputs=[ | |
| gr.Textbox(label="HTMLコード", lines=15, placeholder="<h1>Hello</h1>..."), | |
| gr.Textbox(label="CSSコード", lines=8, placeholder="body { background-color: #fff; }") | |
| ], | |
| outputs=[ | |
| gr.Image(label="フルページスクリーンショット"), | |
| gr.Textbox(label="エラーメッセージ", interactive=False) | |
| ], | |
| title="フルページスクリーンショット(スクロール結合) App", | |
| description="HTMLとCSSを入力すると、ページ全体をスクロールしながらキャプチャして1枚の画像に結合します。" | |
| ) | |
| if __name__ == "__main__": | |
| iface.launch() | |