問題

Chrome の Headless モードで HTML を PNG 画像にキャプチャする際、画面下部に**白い余白(白帯)**が発生することがあります。

google-chrome --headless --screenshot=output.png \
  --window-size=1920,1080 \
  --hide-scrollbars \
  --force-device-scale-factor=1 \
  file:///path/to/slide.html

HTML 側で width: 1920px; height: 1080px を指定しているにもかかわらず、生成された画像の下部に白い帯が残り、bottom で配置した要素(テロップ、フッターなど)が切れてしまいます。

原因

--window-size=1920,1080 はブラウザの外部ウィンドウサイズを指定するオプションであり、実際の**ビューポート(描画領域)**は若干小さくなります。Headless モードでもこのズレは発生します。

つまり:

  • --window-size=1920,1080 → 実際のビューポートは約 1920×1058 程度
  • HTML は 1080px の高さで描画しようとする
  • ビューポートに収まらない下部のコンテンツが見切れる
  • スクリーンショットは 1920×1080 で出力されるが、下部はデフォルト背景色(白)で埋まる

htmlbodyheight: 1080px を指定しても、Chrome が実際に確保するビューポート高さとは一致しないため、CSS だけでは解決できません。

解決方法

ウィンドウサイズを大きめに設定し、スクリーンショット後に Pillow でクロップするのが最も確実です。

1. Chrome のウィンドウサイズを大きくする

google-chrome --headless --screenshot=output.png \
  --window-size=1920,1280 \
  --hide-scrollbars \
  --force-device-scale-factor=1 \
  file:///path/to/slide.html

高さを 1280 に変更することで、1080px のコンテンツが確実にビューポート内に収まります。

2. Pillow で正確にクロップする

from PIL import Image

img = Image.open("output.png")
img = img.crop((0, 0, 1920, 1080))
img.save("output.png")

これにより、余分な下部領域が除去され、正確に 1920×1080 の画像が得られます。

Python での実装例

import subprocess
import tempfile
import os
from PIL import Image


def render_html_to_png(html_content: str, output_png: str):
    """HTML を Chrome Headless で 1920x1080 の PNG にレンダリングする。"""
    with tempfile.NamedTemporaryFile(
        suffix=".html", mode="w", encoding="utf-8", delete=False
    ) as f:
        f.write(html_content)
        html_path = f.name

    try:
        subprocess.run(
            [
                "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
                "--headless",
                "--disable-gpu",
                "--no-sandbox",
                f"--screenshot={os.path.abspath(output_png)}",
                "--window-size=1920,1280",  # 大きめに設定
                "--hide-scrollbars",
                "--force-device-scale-factor=1",
                f"file://{os.path.abspath(html_path)}",
            ],
            capture_output=True,
            timeout=120,
        )
        # 正確に 1920x1080 にクロップ
        if os.path.exists(output_png):
            img = Image.open(output_png)
            img = img.crop((0, 0, 1920, 1080))
            img.save(output_png)
    finally:
        os.unlink(html_path)

HTML 側の推奨構成

html / body のサイズ指定に頼らず、固定サイズの div コンテナですべてのコンテンツを囲むのが安全です。

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
  * { margin: 0; padding: 0; box-sizing: border-box; }
  html, body { margin: 0; padding: 0; overflow: hidden; }

  #stage {
    width: 1920px;
    height: 1080px;
    position: relative;
    overflow: hidden;
    background: linear-gradient(135deg, #e8f5e9, #a5d6a7);
  }

  .footer {
    position: absolute;
    bottom: 30px;
    left: 30px;
    right: 30px;
    /* bottom 配置の要素も確実に表示される */
  }
</style>
</head>
<body>
  <div id="stage">
    <!-- すべてのコンテンツをここに配置 -->
    <div class="footer">テロップなど</div>
  </div>
</body>
</html>

ポイント:

  • #stage に固定の width / height を設定し、position: relative で基準にする
  • html / body のサイズには依存しない
  • overflow: hidden で余分な描画を防止

まとめ

対策内容
Chrome 側--window-size=1920,1280 で高さを余裕を持たせる
後処理Pillow で (0, 0, 1920, 1080) にクロップ
HTML 側固定サイズの #stage コンテナを使う

この組み合わせにより、--screenshot での白帯問題を確実に回避できます。動画生成パイプラインでスライド画像やテロップフレームを大量に生成する場合にも安定して動作します。