Add optional screenshots to bug reports
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user