The Problem

When capturing HTML as PNG images using Chrome’s Headless mode, a white bar appears at the bottom of the output image.

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

Even when the HTML specifies width: 1920px; height: 1080px, the generated image has a white strip at the bottom, and elements positioned with bottom (such as captions, footers, or telops) get clipped.

Root Cause

--window-size=1920,1080 sets the outer window size, not the actual viewport (rendering area). The viewport ends up slightly smaller, even in Headless mode.

What happens:

  • --window-size=1920,1080 → actual viewport is approximately 1920×1058
  • HTML tries to render at 1080px height
  • Content at the bottom extends beyond the viewport
  • The screenshot is output at 1920×1080, but the bottom area is filled with the default background color (white)

Setting height: 1080px on html or body does not help, because Chrome’s actual viewport height doesn’t match.

The Fix

Set a larger window size and crop with Pillow after the screenshot. This is the most reliable approach.

1. Increase Chrome’s Window Size

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

By setting the height to 1280, the 1080px content is guaranteed to fit within the viewport.

2. Crop with Pillow

from PIL import Image

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

This removes the extra area below 1080px, producing an exact 1920×1080 image.

Full Python Example

import subprocess
import tempfile
import os
from PIL import Image


def render_html_to_png(html_content: str, output_png: str):
    """Render HTML to a 1920x1080 PNG using Chrome Headless."""
    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",  # Larger than needed
                "--hide-scrollbars",
                "--force-device-scale-factor=1",
                f"file://{os.path.abspath(html_path)}",
            ],
            capture_output=True,
            timeout=120,
        )
        # Crop to exact 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)

Instead of relying on html / body sizing, wrap all content in a fixed-size div container.

<!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;
    /* Elements with bottom positioning will render correctly */
  }
</style>
</head>
<body>
  <div id="stage">
    <!-- All content goes here -->
    <div class="footer">Caption text</div>
  </div>
</body>
</html>

Key points:

  • #stage has a fixed width / height and serves as the positioning reference with position: relative
  • No dependency on html / body sizing
  • overflow: hidden prevents extra rendering

Summary

LayerFix
Chrome--window-size=1920,1280 — extra height to ensure content fits
Post-processingPillow crop to (0, 0, 1920, 1080)
HTMLUse a fixed-size #stage container instead of relying on html/body

This combination reliably eliminates the white bar issue with --screenshot. It works well for video generation pipelines where you need to produce hundreds of slide or telop frames.