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)
Recommended HTML Structure
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:
#stagehas a fixedwidth/heightand serves as the positioning reference withposition: relative- No dependency on
html/bodysizing overflow: hiddenprevents extra rendering
Summary
| Layer | Fix |
|---|---|
| Chrome | --window-size=1920,1280 — extra height to ensure content fits |
| Post-processing | Pillow crop to (0, 0, 1920, 1080) |
| HTML | Use 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.