octoeverywhere_images.py 1.5 KB

123456789101112131415161718192021222324252627282930313233343536
  1. """Prepare oversized camera snapshots for the Gadget API's image limit."""
  2. from io import BytesIO
  3. from PIL import Image, ImageOps
  4. MAX_IMAGE_BYTES = 6 * 1024 * 1024
  5. MAX_IMAGE_PIXELS = 64_000_000
  6. MAX_IMAGE_DIMENSION = 2048
  7. INVALID_IMAGE_MESSAGE = "Could not prepare the camera snapshot for OctoEverywhere."
  8. def prepare_frame(frame: bytes) -> bytes:
  9. """Keep normal frames unchanged and shrink oversized images to a JPEG."""
  10. if len(frame) <= MAX_IMAGE_BYTES:
  11. return frame
  12. try:
  13. with Image.open(BytesIO(frame)) as source:
  14. if source.width * source.height > MAX_IMAGE_PIXELS:
  15. raise ValueError(INVALID_IMAGE_MESSAGE)
  16. # JPEG decoders can reduce resolution before allocating all pixels.
  17. # Bound the working image before copying or converting its colors.
  18. source.draft("RGB", (MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION))
  19. source.thumbnail((MAX_IMAGE_DIMENSION, MAX_IMAGE_DIMENSION), Image.Resampling.LANCZOS)
  20. with ImageOps.exif_transpose(source) as oriented, oriented.convert("RGB") as image:
  21. for quality in (90, 80, 70):
  22. output = BytesIO()
  23. image.save(output, format="JPEG", quality=quality)
  24. prepared = output.getvalue()
  25. if len(prepared) <= MAX_IMAGE_BYTES:
  26. return prepared
  27. except Exception:
  28. raise ValueError(INVALID_IMAGE_MESSAGE) from None
  29. raise ValueError(INVALID_IMAGE_MESSAGE)