Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 30 additions & 31 deletions src/utils/image_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,22 +89,19 @@ def compute_image_hash(image):
return hashlib.sha256(img_bytes).hexdigest()

def take_screenshot_html(html_str, dimensions, timeout_ms=None):
image = None
try:
# Create a temporary HTML file
with tempfile.NamedTemporaryFile(suffix=".html", delete=False) as html_file:
with tempfile.NamedTemporaryFile(suffix=".html") as html_file:
html_file.write(html_str.encode("utf-8"))
html_file.flush()
html_file_path = html_file.name

image = take_screenshot(html_file_path, dimensions, timeout_ms)

# Remove html file
os.remove(html_file_path)
return take_screenshot(html_file_path, dimensions, timeout_ms)
Comment on lines +94 to +99
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NamedTemporaryFile defaults to delete=True and keeps the file handle open during the with block. Calling take_screenshot(...) while the file is still open can fail on Windows because the browser process may be unable to open/read the temp HTML file. A robust pattern is to create the temp file with delete=False, close it before invoking Chromium, and ensure deletion in a finally (or use a TemporaryDirectory and create a normal file inside it).

Copilot uses AI. Check for mistakes.

except Exception as e:
logger.error(f"Failed to take screenshot: {str(e)}")
Comment on lines 101 to 102
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider using logger.exception("Failed to take screenshot") here so the traceback is captured in logs. This makes debugging subprocess/file-handling failures much easier than logging only str(e).

Suggested change
except Exception as e:
logger.error(f"Failed to take screenshot: {str(e)}")
except Exception:
logger.exception("Failed to take screenshot")

Copilot uses AI. Check for mistakes.
return None

return image

def _find_chromium_binary():
"""Find the first available Chromium-based binary in system PATH."""
Expand All @@ -118,17 +115,15 @@ def _find_chromium_binary():


def take_screenshot(target, dimensions, timeout_ms=None):
image = None
try:
# Find available browser binary
browser = _find_chromium_binary()
if not browser:
logger.error("No Chromium-based browser found. Install chromium, chromium-headless-shell, or chrome.")
return None
# Find available browser binary
browser = _find_chromium_binary()
if not browser:
logger.error("No Chromium-based browser found. Install chromium, chromium-headless-shell, or chrome.")
return None

# Create a temporary output file for the screenshot
with tempfile.NamedTemporaryFile(suffix=".png", delete=False) as img_file:
img_file_path = img_file.name
# Create a temporary output file for the screenshot
with tempfile.NamedTemporaryFile(suffix=".png") as img_file:
img_file_path = img_file.name

Comment on lines +125 to 127
Copy link

Copilot AI Feb 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PNG output temp file is also opened and held by Python while Chromium is expected to write to the same path. On Windows this frequently fails due to file locking, and even on Unix it can be brittle. Prefer creating the output path without holding an open handle (e.g., NamedTemporaryFile(delete=False) then close immediately, or mkstemp() + close), and then remove the file in a finally after loading it.

Copilot uses AI. Check for mistakes.
command = [
browser,
Expand All @@ -146,31 +141,35 @@ def take_screenshot(target, dimensions, timeout_ms=None):
"--disable-gpu-memory-buffer-compositor-resources",
"--disable-extensions",
"--disable-plugins",
"--mute-audio",
"--disable-default-apps",
"--disable-audio-input",
"--disable-audio-output",
"--disable-crash-reporter",
"--disable-login-animations",
"--disable-default-browser-promo",
"--renderer-process-limit=1",
"--no-zygote",
"--no-sandbox"
]
if timeout_ms:
command.append(f"--timeout={timeout_ms}")
result = subprocess.run(command, capture_output=True, check=False)

# Check if the process failed or the output file is missing
if result.returncode != 0 or not os.path.exists(img_file_path):
logger.error(f"Failed to take screenshot (return code: {result.returncode})")
return None
try:
result = subprocess.run(command, capture_output=True, check=False)

# Load the image using PIL
with Image.open(img_file_path) as img:
image = img.copy()
# Check if the process failed or the output file is missing
if result.returncode != 0 or not os.path.exists(img_file_path):
logger.error(f"Failed to take screenshot (return code: {result.returncode})")
return None

# Remove image files
os.remove(img_file_path)
# Load the image using PIL
with Image.open(img_file_path) as img:
return img.copy()

except Exception as e:
logger.error(f"Failed to take screenshot: {str(e)}")
except Exception as e:
logger.error(f"Failed to take screenshot: {str(e)}")
return None

return image

def pad_image_blur(img: Image, dimensions: tuple[int, int]) -> Image:
bkg = ImageOps.fit(img, dimensions)
Expand Down