Source code for ssapy_toolkit.IO.html_to_gif

#!/usr/bin/env python3

import os
import sys
import time
import math
import pathlib
from io import BytesIO

missing = []
try:
    from PIL import Image, ImageChops
except ImportError:
    missing.append("Pillow (PIL) is required. Install with:\n  pip install pillow")

try:
    from selenium import webdriver
    from selenium.webdriver.chrome.options import Options
except ImportError:
    missing.append("Selenium is required. Install with:\n  pip install selenium")

if missing:
    print("Missing required dependencies:\n")
    for msg in missing:
        print(msg + "\n")
    print("After installing the above packages, re-run this script.")
    sys.exit(1)


def _auto_crop_bbox(img, bg_color=None, fuzz=0):
    """
    Return bounding box of non-background pixels.

    bg_color: (R, G, B) or None to auto-detect from top-left pixel.
    fuzz: tolerance for color differences, 0 = exact match.
    """
    img = img.convert("RGB")
    w, h = img.size

    if bg_color is None:
        bg_color = img.getpixel((0, 0))  # assume top-left is background

    # Create solid background image
    bg = Image.new("RGB", (w, h), bg_color)

    # Difference image
    diff = ImageChops.difference(img, bg)
    if fuzz > 0:
        # Expand difference to account for near-bg colors
        diff = diff.point(lambda v: 0 if v <= fuzz else 255)

    bbox = diff.getbbox()
    return bbox  # (left, upper, right, lower) or None if no difference


[docs] def html_to_gif( *, html_path: str, out_gif: str, duration_s: float = 18.0, fps: int = 20, viewport=(1280, 720), wait_after_load_s: float = 0.8, forced_height: int = 2000, # big enough to capture entire animation bg_color=None, # None = auto-detect from top-left fuzz: int = 0, # increase if background not perfectly uniform ): html_path = pathlib.Path(html_path).resolve() if not html_path.exists(): raise FileNotFoundError(str(html_path)) url = html_path.as_uri() nframes = int(math.ceil(duration_s * fps)) raw_frames = [] chrome_options = Options() chrome_options.add_argument("--headless=new") chrome_options.add_argument("--disable-gpu") chrome_options.add_argument("--hide-scrollbars") chrome_options.add_argument(f"--window-size={viewport[0]},{viewport[1]}") chrome_options.binary_location = "/usr/bin/chromium-browser" # adjust if needed driver = None try: driver = webdriver.Chrome(options=chrome_options) driver.get(url) time.sleep(wait_after_load_s) # Force large window; we’ll crop later driver.set_window_size(viewport[0], forced_height) time.sleep(0.3) start = time.time() for i in range(nframes): target_t = start + (i / fps) now = time.time() if target_t > now: time.sleep(target_t - now) png_bytes = driver.get_screenshot_as_png() img = Image.open(BytesIO(png_bytes)).convert("RGB") raw_frames.append(img) finally: if driver is not None: try: driver.quit() except Exception: pass # --- Compute global crop box across all frames --- global_bbox = None for img in raw_frames: bbox = _auto_crop_bbox(img, bg_color=bg_color, fuzz=fuzz) if bbox is None: continue if global_bbox is None: global_bbox = bbox else: l1, t1, r1, b1 = global_bbox l2, t2, r2, b2 = bbox global_bbox = (min(l1, l2), min(t1, t2), max(r1, r2), max(b1, b2)) if global_bbox is None: # No difference from background; just use original frames frames = raw_frames print("Warning: no non-background pixels detected; skipping auto-crop.") else: frames = [img.crop(global_bbox) for img in raw_frames] print("Cropped to bbox:", global_bbox, "-> size", frames[0].size) # --- Save GIF --- os.makedirs(os.path.dirname(out_gif) or ".", exist_ok=True) frames[0].save( out_gif, save_all=True, append_images=frames[1:], duration=int(1000 / fps), loop=0, optimize=False, disposal=2, ) print(f"Wrote GIF: {out_gif} ({len(frames)} frames @ {fps} fps)")
if __name__ == "__main__": home = os.path.expanduser("~") html_to_gif( html_path="/home/yeager7/smart-hpm/scripts/agent/animation.html", out_gif=os.path.join(home, "animation.gif"), duration_s=18.0, fps=20, viewport=(1280, 720), wait_after_load_s=0.8, forced_height=2000, bg_color=None, # or (255, 255, 255) if you know background is white fuzz=5, # tweak if background has slight gradients/antialiasing )