Fix proxy camera crop breaking after a small frame (#183798)

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Filippo Scatigna
2026-10-05 13:51:36 +02:00
committed by GitHub
co-authored by Claude Opus 5.5
parent c16a20897a
commit aec8d56b2d
3 changed files with 105 additions and 13 deletions
+20 -13
View File
@@ -160,18 +160,25 @@ def _crop_image(image, opts):
quality = opts.quality or DEFAULT_QUALITY
(old_width, old_height) = img.size
old_size = len(image)
if opts.top is None:
opts.top = 0
if opts.left is None:
opts.left = 0
if opts.max_width is None or opts.max_width > old_width - opts.left:
opts.max_width = old_width - opts.left
if opts.max_height is None or opts.max_height > old_height - opts.top:
opts.max_height = old_height - opts.top
top = opts.top or 0
left = opts.left or 0
width = old_width - left
if opts.max_width is not None:
width = min(opts.max_width, width)
height = old_height - top
if opts.max_height is not None:
height = min(opts.max_height, height)
if width <= 0 or height <= 0:
_LOGGER.debug(
"Image (%dx%d) is too small to crop at (%d, %d)",
old_width,
old_height,
left,
top,
)
return image
img = img.crop(
(opts.left, opts.top, opts.left + opts.max_width, opts.top + opts.max_height)
)
img = img.crop((left, top, left + width, top + height))
imgbuf = io.BytesIO()
img.save(imgbuf, "JPEG", optimize=True, quality=quality)
newimage = imgbuf.getvalue()
@@ -181,8 +188,8 @@ def _crop_image(image, opts):
old_width,
old_height,
old_size,
opts.max_width,
opts.max_height,
width,
height,
len(newimage),
)
return newimage
+1
View File
@@ -0,0 +1 @@
"""Tests for the Camera Proxy integration."""
+84
View File
@@ -0,0 +1,84 @@
"""Tests for the Camera Proxy camera platform."""
from collections.abc import AsyncGenerator
import io
from unittest.mock import AsyncMock, patch
from PIL import Image as PilImage
import pytest
from homeassistant.components.camera import Image, async_get_image
from homeassistant.components.proxy.camera import ProxyCamera
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.typing import ClientSessionGenerator
CROP_CONFIG = {
"platform": "proxy",
"entity_id": "camera.source",
"name": "Cropped",
"mode": "crop",
"max_image_width": 400,
"max_image_height": 225,
"max_stream_width": 400,
"max_stream_height": 225,
"image_left": 550,
"image_top": 90,
}
def _jpeg(width: int, height: int) -> Image:
buffer = io.BytesIO()
PilImage.new("RGB", (width, height)).save(buffer, "JPEG")
return Image("image/jpeg", buffer.getvalue())
def _size(content: bytes) -> tuple[int, int]:
return PilImage.open(io.BytesIO(content)).size
@pytest.fixture
async def setup_proxy(hass: HomeAssistant) -> AsyncGenerator[None]:
"""Set up a cropping proxy camera with no delay between stream frames."""
with patch.object(ProxyCamera, "_attr_frame_interval", 0):
assert await async_setup_component(hass, "camera", {"camera": CROP_CONFIG})
await hass.async_block_till_done()
yield
@pytest.mark.usefixtures("setup_proxy")
async def test_crop_image_after_small_frame(hass: HomeAssistant) -> None:
"""Test a frame smaller than the crop area does not break later crops."""
small = _jpeg(320, 180)
with patch(
"homeassistant.components.proxy.camera.async_get_image",
AsyncMock(side_effect=[small, _jpeg(1536, 576)]),
):
first = await async_get_image(hass, "camera.cropped")
second = await async_get_image(hass, "camera.cropped")
assert first.content == small.content
assert _size(second.content) == (400, 225)
@pytest.mark.usefixtures("setup_proxy")
async def test_crop_stream_after_small_frame(
hass: HomeAssistant, hass_client: ClientSessionGenerator
) -> None:
"""Test a small frame does not break the cropped MJPEG stream."""
client = await hass_client()
with patch(
"homeassistant.components.proxy.camera.async_get_image",
AsyncMock(side_effect=[_jpeg(320, 180), _jpeg(1536, 576), None]),
):
response = await client.get("/api/camera_proxy_stream/camera.cropped")
body = await response.read()
assert response.status == 200
frames = [
part.split(b"\r\n\r\n", 1)[1].rstrip(b"\r\n")
for part in body.split(b"--frameboundary\r\n")
if b"\r\n\r\n" in part
]
assert _size(frames[-1]) == (400, 225)