Add optional screenshots to bug reports

This commit is contained in:
2026-09-15 21:16:06 +02:00
parent 39af9ecdb4
commit 5d3ae10036
10 changed files with 263 additions and 12 deletions
+65
View File
@@ -0,0 +1,65 @@
"""Decode and re-encode small raster uploads before they leave MetalCircle."""
from io import BytesIO
import warnings
from PIL import Image, ImageOps
MAX_SCREENSHOT_BYTES = 4 * 1024 * 1024
MAX_SCREENSHOT_PIXELS = 16_000_000
MAX_SCREENSHOT_OUTPUT_BYTES = 8 * 1024 * 1024
class ScreenshotError(ValueError):
"""Stable validation code; never includes the uploaded filename or bytes."""
def __init__(self, code):
self.code = code
super().__init__(code)
def sanitize_screenshot(contents):
if not contents:
raise ScreenshotError('empty')
if len(contents) > MAX_SCREENSHOT_BYTES:
raise ScreenshotError('too_large')
try:
with warnings.catch_warnings():
warnings.simplefilter('error', Image.DecompressionBombWarning)
with Image.open(BytesIO(contents)) as candidate:
if candidate.format not in {'JPEG', 'PNG', 'WEBP'}:
raise ScreenshotError('unsupported')
if candidate.width * candidate.height > MAX_SCREENSHOT_PIXELS:
raise ScreenshotError('too_large')
if getattr(candidate, 'n_frames', 1) != 1:
raise ScreenshotError('unsupported')
candidate.verify()
with Image.open(BytesIO(contents)) as candidate:
if candidate.width * candidate.height > MAX_SCREENSHOT_PIXELS:
raise ScreenshotError('too_large')
image = ImageOps.exif_transpose(candidate)
image.load()
has_alpha = image.mode in {'RGBA', 'LA'} or (
image.mode == 'P' and 'transparency' in image.info
)
if image.mode not in {'RGB', 'RGBA'}:
image = image.convert('RGBA' if has_alpha else 'RGB')
image.thumbnail((1600, 1600))
output = BytesIO()
if image.mode == 'RGBA':
image.save(output, format='PNG', optimize=True)
extension, media_type = 'png', 'image/png'
else:
image.save(output, format='JPEG', quality=85, optimize=True)
extension, media_type = 'jpg', 'image/jpeg'
safe_contents = output.getvalue()
except ScreenshotError:
raise
except Exception:
# Decoders may raise different exception types for malformed inputs.
# Fail closed without exposing decoder details to the caller.
raise ScreenshotError('invalid') from None
if len(safe_contents) > MAX_SCREENSHOT_OUTPUT_BYTES:
raise ScreenshotError('too_large')
return safe_contents, extension, media_type